背景
前两天参加一个面试,面试官给了这样几道题。
// 题目一
// 有字符串 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
。 - 如果
limit
为0
,则返回[]
。
- 一个非负整数,指定数组中包含的子字符串的数量限制。当提供此参数时,split 方法会在指定
// 使用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
。
✨ includes
和indexOf
类似,includes
返回布尔值,indexOf
返回位置索引,没找到的话返回-1.
String.prototype.substring()/String.prototype.slice()
// 语法
substring(indexStart)
substring(indexStart, indexEnd)
slice(indexStart)
slice(indexStart, indexEnd)
这两个方法其实是很相似的。都是可以接受两个参数,都是左闭右开区间(不会截取end位置上的字符),不会对原字符串产生影响,返回新字符串。
区别在于:
substring()
方法在indexStart
大于indexEnd
的情况下会交换它的两个参数,这意味着仍会返回一个字符串。而slice()
方法在这种情况下返回一个空字符串。- 如果两个参数中的任何一个或两个都是负数或
NaN
,substring()
方法将把它们视为0
。更具体的说是任何小于0
或大于str.length
的参数值都会被视为分别等于0
和str.length
。任何值为NaN
的参数将被视为等于0
。substring
不会从尾部开始计算。 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
开头的不改变原数组的方法。
改变原数组方法 | 不改变原数组方法 |
---|---|
sort | toSorted |
reverse | toReversed |
splice | toSpliced |
array[i]=newValue | array.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上查看。
如有问题,欢迎指正~