汇总自我的公众号上的三篇关于数组拷贝的文章:
我们这里不谈 深拷贝 和 浅拷贝 ,就拿一个具体的例子来说,有一个数组 a
,我们想得到一个和它一样的数组 b
:
// 数组 a
const a = [1, 2, 3];
// 经过某种操作得到数组 b,b的内容 和 a 一样
const b = ?;
评测的标准:
a
的内容 和 b
的内容 一样本文使用 jest
进行测试。
slice
方法不传参时,返回一个和原数组内容一样的新数组。
test('slice', () => {
const b = a.slice();
expect(a).toEqual(b);
expect(a).not.toBe(b);
});
test('spread', () => {
const b = [...a];
expect(a).toEqual(b);
expect(a).not.toBe(b);
});
test('concat', () => {
const b = a.concat();
expect(a).toEqual(b);
expect(a).not.toBe(b);
});
注意 map
的入参,我们需要一个返回自身入参的函数。
test('map', () => {
const b = a.map(v => v);
expect(a).toEqual(b);
expect(a).not.toBe(b);
});
test('Array.from', () => {
const b = Array.from(a);
expect(a).toEqual(b);
expect(a).not.toBe(b);
});
test('assign', () => {
const b = Object.assign([], a);
expect(a).toEqual(b);
expect(a).not.toBe(b);
});
test('for', () => {
const b = [];
for (const e of a) {
b.push(e);
}
expect(a).toEqual(b);
expect(a).not.toBe(b);
});
test('filter', () => {
const b = a.filter(_ => true);
expect(a).toEqual(b);
expect(a).not.toBe(b);
});
凡是 filter 和 map 能做的,reduce 也一定可以。
test('reduce', () => {
const b = a.reduce((p, v) => (p.push(v), p), []);
// 或者
const bb = a.reduce((p, v) => p.concat(v), []);
expect(a).toEqual(b);
expect(a).not.toBe(b);
});
以上可说是基础的部分,下面的基本上就很少会用到了。
test('json', () => {
const b = JSON.parse(JSON.stringify(a));
expect(a).toEqual(b);
expect(a).not.toBe(b);
});
test('Object.values', () => {
const b = Object.values(a);
expect(a).toEqual(b);
expect(a).not.toBe(b);
});
test('Object.entries', () => {
const b = Object.entries(a).map(v => v[1]);
expect(a).toEqual(b);
expect(a).not.toBe(b);
});
注意,它和 Object.values
不同,它返回的是一个 iterator
对象。
test('Array.values', () => {
const b = [...a.values()];
expect(a).toEqual(b);
expect(a).not.toBe(b);
});
test('Array.keys', () => {
const b = [...a.keys()].map(k => a[k]);
expect(a).toEqual(b);
expect(a).not.toBe(b);
});
test('Object.keys', () => {
const b = Object.keys(a).map(k => a[k]);
expect(a).toEqual(b);
expect(a).not.toBe(b);
});
unshift
把入参追加到数组的头部,并返回数组新的长度。
注意它和 push
的区别,push
是把入参追加到数组的尾部。
test('unshift', () => {
const b = [];
b.unshift(...a);
expect(a).toEqual(b);
expect(a).not.toBe(b);
});
test('Array()', () => {
const b = new Array(...a);
expect(a).toEqual(b);
expect(a).not.toBe(b);
});
当数组a
的长度是1时,该方法是有陷阱的:
new Array(...['x']); // ["x"],符合要求
new Array(...[2]); // [empty × 2],而不是 [2]
new Array(...[1]); // [empty],而不是 [1]
new Array(...[2.0]); // [empty × 2],而不是 [2.0]
new Array(...[2.1]); // Uncaught RangeError: Invalid array length
原因是,当 Array
作为构造器使用时,且入参只有一个时,它会判断 入参是否是数值,如果是,会进一步判断是否是正整数值,如果是,则创建一个长度为该值的数组,否则报RangeError
;如果入参不是数值,则创建一个包含入参的新数组。
该方法更像是 Array
构造器 的安全版本,无论入参有几个,都能如期返回我们想要的数组。
test('Array.of', () => {
const b = Array.of(...a);
expect(a).toEqual(b);
expect(a).not.toBe(b);
});
Array.of
有更多的用途。
该方法不依赖 this
,因此可以独立使用:
const of = Array.of;
const a = of(1, 2, 3); // [1, 2, 3]
可以提供一个 bind
版本,预填充一些数据:
const rows = [
[1, 2, 3],
[4, 5, 6]
];
const boundOf = Array.of.bind(null, 6, 6, 6);
// [[6, 6, 6, 1, 2, 3],[6, 6, 6, 4, 5, 6]]
const rowsNew = rows.map(v => boundOf(...v));
甚至可以提供一个自定义的的 constructor
供 Array.of
实例化返回值:
function Test() {
}
Test.prototype.last = function () {
return this[this.length - 1]
};
const what = Array.of.call(Test, 'a', 'b', "the last");
console.log(what);
console.log(what.last());
运行上述代码,Chrome打印如下信息:
Test {0: "a", 1: "b", 2: "the last", length: 3}
"the last"
有了以上理解,我们甚至可以方便实现一个 Array
子类,请读者行尝试。
一般的认识是,该方法用于将一个含有多维元素的数组展开为1维。但可以变个法子玩,将入参 depth
设置为 0,不进行 「降维」 操作:
test('Array.flat', () => {
const b = a.flat(0);
expect(a).toEqual(b);
expect(a).not.toBe(b);
});
结合之前的介绍,Array.map
和 Array.flat
都可以进行数组拷贝操作,那 Array.flatMap
可以吗?
当然了:
test('Array.flatMap', () => {
const b = a.flatMap(v => [v]);
expect(a).toEqual(b);
expect(a).not.toBe(b);
});
注意入参 v => [v]
的使用,如果你使用的是 v => v
,那 [1, [2, 3]]
将转换为 [1, 2, 3]
,很明显就不符合拷贝的要求了。
到目前为止介绍了 20 种数组拷贝方法,并没有明确说明它们之间的区别,这里简单提一下供读者思考。
JS中有一种 Array
叫 sparse array,其特征是某些位置缺失索引,这些位置又叫 holes
或 empty slots
,在 Chrome console 下会显示为 empty:
var a = [];
a.length = 3;
var b = [0, , 2];
console.log(a); // [empty × 3]
console.log(b); // [0, empty, 2]
可以使用 Object.keys
去查看以上数组的 keys:
console.log(Object.keys(a)); // []
console.log(Object.keys(b)); // ["0", "2"]
能清晰的看到,虽然 a
的长度为 3,但 keys
是空;同理 b
的长度也为 3,但 keys
只有 0 和 2,1 没了。
对于 sparse array,JS 中不同的 Array
方法处理方式是不同的,以 Array.from
和 Array.flat
为例:
var a = [];
a.length = 3;
console.log(a); // [empty × 3]
console.log(Array.from(a)); // [undefined, undefined, undefined]
console.log(a.flat(0)); // []
因此,在选择哪一种数组拷贝方法时,需要考虑要处理的数组是否是 sparse array 以及你要怎样处理 empty slots,以免掉入陷阱。