 
汇总自我的公众号上的三篇关于数组拷贝的文章:
我们这里不谈 深拷贝 和 浅拷贝 ,就拿一个具体的例子来说,有一个数组 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,以免掉入陷阱。