JS中字符串/数组原生方法--JS手写代码题(三)

74 阅读11分钟

背景

前两天参加一个面试,面试官给了这样几道题。

// 题目一
// 有字符串 var str = 'abc345efgabcab',请写出 3 条 JS 语句分别实现如下 3 个功能(使用正则):
// 1)去掉字符串中的a、b、c 字符,形成结果:'345efg'
// 2)将字符串中的数字用中括号括起来,形成结果:'abc[345]efgabcab'
// 3)将字符串中的每个数字的值分别乘以 2,形成结果:'abc6810efgabcab'


// 题目二
// 请写出以下程序的输出
const arr = [3, 1, 4, 1, 5, 9];
const sortedArr = arr.sort((a, b) => a - b);
console.log(sortedArr);
console.log(arr);


// 题目三
// 多种方式实现多层嵌套数组扁平化

题目都不是很难,但是还蛮考验基础知识的应用的。

第一题

第一题可以使用字符串replace方法搭配正则来进行解决。

// 有字符串 var str = 'abc345efgabcab',请写出 3 条 JS 语句分别实现如下 3 个功能(使用正则):
// 1)去掉字符串中的a、b、c 字符,形成结果:'345efg'
// 这题会简单一点,直接正则匹配,替换为空即可
str.replace(/[abc]/g, '')

// 2)将字符串中的数字用中括号括起来,形成结果:'abc[345]efgabcab'
// 第二题,也是可以利用正则来解决,$1代替第一个捕获组
str.replace(/(\d+)/g, '[$1]')

// 3)将字符串中的每个数字的值分别乘以 2,形成结果:'abc6810efgabcab'
// ❗这题我没写出来,其实replace的第二个参数还可以是一个函数,返回值会替换对应的捕获组
str.replace(/(\d+)/, (match) => match * 2)

replace方法的对应详细使用可以继续往下看。

第二题

这题很简单,我们都知道sort是会改变原数组的,所以返回值和原数组都变成了升序排列。

[1,1,3,4,5,9]
[1,1,3,4,5,9]

但是面试官接着问,你知道数组有什么排序方法是不改变原数组的吗?

我没答上来😥

第三题

用多种方式实现多层嵌套数组扁平化。

我第一反应是数组有flat方法可以打平多层嵌套的数组,而且我记得可以传一个层数来控制打平的层数,但是完全打平不确定层次我记不清是,不传参数默认值就是完全打平,还是传个-1又或是0,又或是Infinity

第二个解题思路是,自己手写一个完全打平的方法,利用递归。

// 2. 自定义函数
function flatArray(arr) {
  const result = []
  for (let i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      // 递归
      result.push(...flatArray(arr[i]))
    } else {
      result.push(arr[i])
    }
  }
  return result
}

感觉上面题目都不难,但是呢都没办法很顺畅、清楚的一次性完美解答。反应出来对基础api还不是很熟练,对后面新增的一些api不太了解。

所以想着说,借着这个机会,整理一波JavaScript中字符串和数组的原生方法。

字符串方法

String.prototype.replace

// 语法
replace(pattern, replacement)
  • patter

    • 可以是字符串或者带有或者一个带有 Symbol.replace 方法的对象,典型的例子就是正则表达式
  • replacement

    • 如果是字符串,它将替换由 pattern 匹配的子字符串。支持一些特殊的替换模式
    • 如果是函数,将为每个匹配调用该函数,并将其返回值用作替换文本。
  • 返回值

    • 一个新的字符串,该方法并不改变调用它的字符串本身。
const str = "abc123def"
// 如果pattern是个普通字符串,那么替换第一个找到的字符串
str.replace('abc', 'a') // 'a123def'

// 如果 pattern 是一个空字符串,则替换项将被插入到字符串的开头。
"xxx".replace("", "_") // "_xxx"


// 如果 pattern 参数是个正则表达式
str.replace(/abc/, 'a') //'a123abcdef'

// 正则表达式不带g只会替换第一个,带了g表示全局替换
str.replace(/abc/g, 'a') // 'a123adef'

// 带有捕获组的正则的话,replacement可以使用一些特殊模式
// $1, $2, $···, $n, 代表第几个捕获组,从1开始
// 这里就相当于给每个捕获组添加一对[]
str.replace(/(abc)/g, '[$1]') // '[abc]123[abc]def'

// 如果replacement是一个函数,函数第一个参数是对应的捕获组
str.replace(/(abc)/g, (match) => `[${match}]`) // '[abc]123[abc]def'

replacement的函数入参有很多。具体可以查看 MDN.

String.prototype.replaceAll()

入参和replace方法都是一样的,不同之处在于replace只会默认替换掉第一个匹配的字符串,而replaceAll是默认替换掉所有满足的匹配的字符串。

还有一点要注意:如果 pattern 是一个正则表达式,则必须设置全局( g )标志,否则会抛出 TypeError

String.prototype.split()

// 语法
split(separator)
// split其实是可以接受第二个参数的
split(separator, limit)
  • separator

    • 可以是字符串,可以是正则表达式
  • limit

    • 一个非负整数,指定数组中包含的子字符串的数量限制。当提供此参数时,split 方法会在指定 separator 每次出现时分割该字符串,但在已经有 limit 个元素时停止分割。任何剩余的文本都不会包含在数组中。
    • 如果在达到极限之前就达到了字符串的末端,那么数组包含的条目可能少于 limit
    • 如果 limit0,则返回 []
// 使用limit限制数组长度,超出数组长度的部分将会被忽略
const myString = "Hello World. How are you doing?";
myString.split(" ", 3); // ["Hello", "World.", "How"]

// 使用正则
const myString = "Hello 1 word. Sentence number 2.";
myString.split(/\d/); // ["Hello ", " word. Sentence number ", "."]
// ✨ 正常来说,分隔符不会保留在数组中,但是如果正则中使用了捕获组,那么分隔符也会保留在数组中
myString.split(/(\d)/); // ["Hello ", "1", " word. Sentence number ", "2", "."]

String.prototype.padStart()/String.prototype.padEnd()

// 语法
padStart(targetLength)
padStart(targetLength, padString)
  • targetLength

当前 str 填充后的长度。如果该值小于或等于 str.length,则会直接返回当前 str

  • padString 可选

用于填充当前 str 的字符串。如果 padString 太长,无法适应 targetLength,则会从末尾被截断。默认值为 Unicode“空格”字符(U+0020)。

"abc".padStart(10); // "       abc"
"abc".padStart(10, "foo"); // "foofoofabc"
"abc".padStart(6, "123465"); // "123abc"
"abc".padStart(8, "0"); // "00000abc"
"abc".padStart(1); // "abc"

padEnd和padStart入参一致,只是一个在尾部填充,一个在头部填充而已。

String.prototype.at/CharAt/CharCodeAt

  • at方法就是返回下标处的单个字符,和直接用下标取值也是一样的。不同的是at返回可以接受一个负数,表示从尾部开始取。-1 表示最后一个字符,-2 表示倒数第二个字符,以此类推。
  • charAt方法和at方法类似,但是不接受负数。传入任何不在[0,str.length-1]的数都会返回空字符串。
  • CharCodeAt返回指定位置的字符的 Unicode 代码点。同样接受一个表示字符位置的整数参数,从 0 开始计数。如果参数超出了字符串的长度范围,则返回NaN。
// ✨ 可以直接用下标取值,类似于数组,更加方便
const str = "Hello World";
str[0] // "H"

const str = "Hello World";
console.log(str.at(0)); // "H"
console.log(str.at(-1)); // "d"

const str = "Hello World";
console.log(str.charAt(0)); // "H"
console.log(str.charAt(100)); // ""

const str = "A";
console.log(str.charCodeAt(0)); // 65

String.prototype.includes()

// 语法
includes(searchString)
includes(searchString, position)
  • searchString

一个要在 str 中查找的字符串。不能是正则表达式。如果 searchString 是一个正则表达式,则会抛出异常。

  • position 可选

在字符串中开始搜索 searchString 的位置。默认值为 0

includesindexOf类似,includes返回布尔值,indexOf返回位置索引,没找到的话返回-1.

String.prototype.substring()/String.prototype.slice()

// 语法
substring(indexStart)
substring(indexStart, indexEnd)

slice(indexStart)
slice(indexStart, indexEnd)

这两个方法其实是很相似的。都是可以接受两个参数,都是左闭右开区间(不会截取end位置上的字符),不会对原字符串产生影响,返回新字符串。

区别在于:

  • substring() 方法在 indexStart 大于 indexEnd 的情况下会交换它的两个参数,这意味着仍会返回一个字符串。而 slice() 方法在这种情况下返回一个空字符串。
  • 如果两个参数中的任何一个或两个都是负数或 NaNsubstring() 方法将把它们视为 0。更具体的说是任何小于 0 或大于 str.length 的参数值都会被视为分别等于 0str.length。任何值为 NaN 的参数将被视为等于 0substring不会从尾部开始计算。
  • slice() 方法也将 NaN 参数视为 0,但当给定负值时,它会从字符串的末尾开始反向计数以找到索引。
// 这里把负数都当成了0,所以才会有如下结果
console.log(text.substring(-5, 2)); // "Mo"
console.log(text.substring(-5, -2)); // ""

// slice接受负数参数,表示从末尾开始
console.log(text.slice(-5, 2)); // ""
console.log(text.slice(-5, -2)); // "zil"

还有一个函数是String.prototype.substr(), 它也是接受两个参数,不同的是第二个参数表示要截取的长度。但是已经不再推荐使用了,尽量别用了吧。

数组方法

Array.prototype.sort()/Array.prototype.toSorted()

sort() 方法*就地*对数组的元素进行排序,并返回对相同数组的引用。默认排序是将元素转换为字符串,然后按照它们的字典值升序排序。

这里主要想提的一点就是toSorted()方法.它的使用方法和sort()方法一致,唯一的区别就在于toSorted()方法不会改变原数组。所以上面面试第二题追问的什么方法不会改变原数组,答案就是这个。

相应的下面的方法都新增了一个对应的带to开头的不改变原数组的方法。

改变原数组方法不改变原数组方法
sorttoSorted
reversetoReversed
splicetoSpliced
array[i]=newValuearray.with(1, newValue)

Array.prototype.with()

Array 实例的 with() 方法是使用方括号表示法修改指定索引值的复制方法版本。它会返回一个新数组,其指定索引处的值会被新值替换。

// 对原数组没有影响
const arr = [1, 2, 3, 4, 5];
console.log(arr.with(2, 6)); // [1, 2, 6, 4, 5]
console.log(arr); // [1, 2, 3, 4, 5]

// 相当于,
const arr = [1, 2, 3, 4, 5];
const arrcopy = arr.slice()
arrcopy[2] = 6
// 所以说:实例的 with() 方法是使用方括号表示法修改指定索引值的复制方法版本。

Array.prototype.flat()

flat() 方法创建一个新的数组,并根据指定深度递归地将所有子数组元素拼接到新的数组中。

flat(depth?)

depth 可选

指定要提取嵌套数组的结构深度,默认值为 1。flat() 方法属于复制方法。它不会改变原数组,而是返回一个浅拷贝,该浅拷贝包含了原始数组中相同的元素。

const arr1 = [1, 2, [3, 4]];
arr1.flat();
// [1, 2, 3, 4]

const arr2 = [1, 2, [3, 4, [5, 6]]];
arr2.flat();
// [1, 2, 3, 4, [5, 6]]

const arr3 = [1, 2, [3, 4, [5, 6]]];
arr3.flat(2);
// [1, 2, 3, 4, 5, 6]

const arr4 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]];
arr4.flat(Infinity);
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Array.prototype.flatMap()

flatMap() 方法对数组中的每个元素应用给定的回调函数,然后将结果展开一级,返回一个新数组。它等价于在调用 map() 方法后再调用深度为 1 的 flat() 方法(arr.map(...args).flat()),但比分别调用这两个方法稍微更高效一些。

const arr = [1, 2, 3, 4];

arr.flatMap((x) => [x, x * 2]);
// 等价于
const n = arr.length;
const acc = new Array(n * 2);
for (let i = 0; i < n; i++) {
  const x = arr[i];
  acc[i * 2] = x;
  acc[i * 2 + 1] = x * 2;
}
// [1, 2, 2, 4, 3, 6, 4, 8]

flatMap只会展开1级,并没法指定展开的层级数目。

map() 方法过程中添加和删除元素

flatMap 方法可以用作在 map 方法中添加和删除元素(修改元素数量)的方法。如果要保留该项,则返回一个包含该项的单元素数组,如果要添加元素,则返回一个包含多个元素的数组,如果要删除该项,则返回一个空数组。flatMap最后的结果都会展平,所以相当于添加或者删除元素了。

// 假设我们想要删除所有负数,并将奇数拆分成偶数和 1
const a = [5, 4, -3, 20, 17, -33, -4, 18];
//         |\  \  x   |  | \   x   x   |
//        [4,1, 4,   20, 16, 1,       18]

const result = a.flatMap((n) => {
  if (n < 0) {
    return [];
  }
  return n % 2 === 0 ? [n] : [n - 1, 1];
});
console.log(result); // [4, 1, 4, 20, 16, 1, 18]

Array.prototype.copyWithin()

copyWithin() 方法浅复制数组的一部分到同一数组中的另一个位置,并返回它,不会改变原数组的长度

console.log([1, 2, 3, 4, 5].copyWithin(-2));
// [1, 2, 3, 1, 2]

console.log([1, 2, 3, 4, 5].copyWithin(0, 3));
// [4, 5, 3, 4, 5]

console.log([1, 2, 3, 4, 5].copyWithin(0, 3, 4));
// [4, 2, 3, 4, 5]

console.log([1, 2, 3, 4, 5].copyWithin(-2, -3, -1));
// [1, 2, 3, 3, 4]

Array.from()/Array.of()

Array.from() 静态方法从可迭代或类数组对象创建一个新的浅拷贝的数组实例。

Array.of() 静态方法通过可变数量的参数创建一个新的 Array 实例,而不考虑参数的数量或类型。

// 字符串是可迭代对象,能够直接转换为数组
Array.from('foo') // ["f", "o", "o"]

Array.from(new Set())

const mapper = new Map()
// map的values返回的是迭代器对象,并不是一个数组,要转换一下才能使用数组方法
Array.from(mapper.values())


// Array.of
Array.of('foo', 2, 'bar', true) // ["foo", 2, "bar", true]

这里只简单列举了部分字符串和数组的方法,全部方法可到MDN上查看。

如有问题,欢迎指正~

上一篇:Promise静态方法实现--JS手写代码题(二)

参考文档

Array.from() - JavaScript | MDN