一个叫木头,一个叫马尾

JavaScript数组拷贝的20种方法

汇总自我的公众号上的三篇关于数组拷贝的文章:




我们这里不谈 深拷贝 和 浅拷贝 ,就拿一个具体的例子来说,有一个数组 a,我们想得到一个和它一样的数组 b

// 数组 a
const a = [123];

// 经过某种操作得到数组 b,b的内容 和 a 一样
const b = ?;

评测的标准:

  1. a的内容 和 b的内容 一样
  2. a !== b

本文使用 jest 进行测试。


1. slice

slice方法不传参时,返回一个和原数组内容一样的新数组。

test('slice'() => {
  const b = a.slice();

  expect(a).toEqual(b);
  expect(a).not.toBe(b);
});

2. spread

test('spread'() => {
  const b = [...a];
  
  expect(a).toEqual(b);
  expect(a).not.toBe(b);
});

3. concat

test('concat'() => {
  const b = a.concat();

  expect(a).toEqual(b);
  expect(a).not.toBe(b);
});

4. map

注意 map 的入参,我们需要一个返回自身入参的函数。

test('map'() => {
  const b = a.map(v => v);

  expect(a).toEqual(b);
  expect(a).not.toBe(b);
});

5. Array.from

test('Array.from'() => {
  const b = Array.from(a);

  expect(a).toEqual(b);
  expect(a).not.toBe(b);
});

6. Object.assign

test('assign'() => {
  const b = Object.assign([], a);

  expect(a).toEqual(b);
  expect(a).not.toBe(b);
});

7. 传统的for循环(while或递归也行)

test('for'() => {
  const b = [];
  for (const e of a) {
    b.push(e);
  }

  expect(a).toEqual(b);
  expect(a).not.toBe(b);
});

8. filter

test('filter', () => {
  const b = a.filter(_ => true);

  expect(a).toEqual(b);
  expect(a).not.toBe(b);
});

9. reduce

凡是 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);
});

以上可说是基础的部分,下面的基本上就很少会用到了。

10. JSON.parse

test('json'() => {
  const b = JSON.parse(JSON.stringify(a));
  
  expect(a).toEqual(b);
  expect(a).not.toBe(b);
});

11. Object.values

test('Object.values'() => {
  const b = Object.values(a);

  expect(a).toEqual(b);
  expect(a).not.toBe(b);
});

12. Object.entries

test('Object.entries'() => {
  const b = Object.entries(a).map(v => v[1]);

  expect(a).toEqual(b);
  expect(a).not.toBe(b);
});

13. Array.values

注意,它和 Object.values 不同,它返回的是一个 iterator 对象。

test('Array.values'() => {
  const b = [...a.values()];

  expect(a).toEqual(b);
  expect(a).not.toBe(b);
});

14. Array.keys

test('Array.keys'() => {
  const b = [...a.keys()].map(k => a[k]);

  expect(a).toEqual(b);
  expect(a).not.toBe(b);
});

15. Object.keys

test('Object.keys'() => {
  const b = Object.keys(a).map(k => a[k]);

  expect(a).toEqual(b);
  expect(a).not.toBe(b);
});

16. unshift

unshift 把入参追加到数组的头部,并返回数组新的长度。
注意它和 push 的区别,push 是把入参追加到数组的尾部。

test('unshift'() => {
  const b = [];
  b.unshift(...a);

  expect(a).toEqual(b);
  expect(a).not.toBe(b);
});

17. Arrray 构造器

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;如果入参不是数值,则创建一个包含入参的新数组。

18. Array.of

该方法更像是 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(123); // [1, 2, 3]

可以提供一个 bind 版本,预填充一些数据:

const rows = [
  [123],
  [456]
];

const boundOf = Array.of.bind(null666);
// [[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"length3}
"the last"

有了以上理解,我们甚至可以方便实现一个 Array 子类,请读者行尝试。

子类参考

19. Array.flat

一般的认识是,该方法用于将一个含有多维元素的数组展开为1维。但可以变个法子玩,将入参 depth 设置为 0,不进行 「降维」 操作:

test('Array.flat'() => {
  const b = a.flat(0);

  expect(a).toEqual(b);
  expect(a).not.toBe(b);
});

20. Array.flatMap

结合之前的介绍,Array.mapArray.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中有一种 Arraysparse array,其特征是某些位置缺失索引,这些位置又叫 holesempty 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.fromArray.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,以免掉入陷阱。