【译】在 JavaScript 中创建和填充任意长度的数组

6,529 阅读5分钟

原文链接:2ality.com/2018/12/cre…

创建数组的最佳方式是使用字面量:

const arr = [0,0,0];

但也不总是最佳方案,例如在创建大型数组时。这篇博客就探讨了在这些情况下该怎么做。

1. 没有空元素的数组往往表现更好

在大多数编程语言中,数组是连续的值序列。 在 JavaScript 中,Array 是一个将索引映射到元素的字典,它可以有空元素,而且空元素也有自己对应的索引,但它并不映射到元素上。例如下面这个数组就有一个空元素在索引 1 的位置上。

> Object.keys(['a',, 'c'])
[ '0', '2' ]

没有空元素的数组称作密数组,密数组往往表现更好,因为它们可以连续(内部)存储。一旦至少有一个空元素,内部表示必须改变。有两种选择:

  • 字典,查找需要更多时间并且存储开销更大。
  • 连续的数据结构,具有空元素的标记值。检查值是否是一个空元素,需要额外的时间。在任何一种情况下,如果引擎遇到一个空元素,它不能只返回 undefined ,而必须遍历原型链并搜索一个名称为空元素索引的属性,这需要花费更多时间。

在某些引擎中,例如 V8,切换到性能较低的数据结构是永久性的。即使所有空元素都添加上值,它们也不会切换回来。

有关 V8 如何表现数组的更多信息,请参阅 Mathias Bynens 的 V8 中的元素类型

2. 创建数组

数组构造函数

使用数组构造函数来创建一个给定长度的数组是一个很普遍的事情。

const LEN = 3;
const arr = new Array(LEN);
assert.equal(arr.length, LEN);
// arr only has holes in it
assert.deepEqual(Object.keys(arr), []);

这种方法很方便,但它有两个缺点:

  • 即使您稍后用完全添上值,这些空元素也会使此数组略微变慢。
  • 空元素很少是元素的初始“值”。例如,0 更常见。

2.2 数组构造函数加上 .fill() 方法

.fill() 方法改变现有数组并使用指定值填充它。这有助于在通过新的 Array() 创建数组后初始化数组:

const LEN = 3;
const arr = new Array(LEN).fill(0);
assert.deepEqual(arr,[0,0,0]);

警告:如果你 .fill() 一个带有对象的数组,所有元素都引用同一个实例(即不会克隆该对象):

const LEN = 3;
const obj = {};

const arr = new Array(LEN).fill(obj);
assert.deepEqual(arr, [{}, {}, {}]);

obj.prop = true;
assert.deepEqual(arr,
  [ {prop:true}, {prop:true}, {prop:true} ]);

我们稍后会遇到一种没有这个问题的填充方法(通过 Array.from())。

2.3 .push() 方法

const LEN = 3;shu
const arr = [];
for (let i=0; i < LEN; i++) {
  arr.push(0);
}
assert.deepEqual(arr, [0, 0, 0]);

这一次,我们创建并填充了一个数组而没有在其中添加空元素。因此,在创建数组后使用数组应该比使用数组构造函数更快。唉,创建数组的速度较慢,因为引擎不得不随着它的增长多次重新分配连续的内部表示。

2.4 填充未定义的数组

Array.from()iterables类型 和 like-Array 类型 的值转换为数组。它将空元素视为未定义的元素。我们可以使用它将每个空元素转换为未定义的:

> Array.from({length: 3})
[ undefined, undefined, undefined ]

参数 {length:3} 是一个类似于数组的对象,长度为3,只包含空元素。也可以使用 new Array(3) ,但通常会创建更大的对象。

数组扩展仅适用于 iterable 的值,并且与 Array.from() 具有类似的效果:

> [...new Array(3)]
[ undefined, undefined, undefined ] 

唉,Array.from() 通过 new Array() 创建它的结果,所以你仍然得到一个稀疏数组。

2.5 使用 Array.from() 映射

如果提供映射函数作为其第二个参数,则可以使用 Array.from() 进行映射。

使用值填充数组

  • 创建一个小整数的数组:
> Array.from({length:3},()=> 0)
[0,0,0]
  • 使用唯一(非共享)对象创建数组:
> Array.from({length:3},()=>({}))
[{},{},{}]

创建整数值范围

  • 使用升序整数创建数组:
> Array.from({length:3},(x,i)=> i)
[0,1,2]
  • 创建任意整数范围:
> const START = 2,END = 5;
> Array.from({length:END-START},(x,i)=> i + START)
[2,3,4]
  • 使用升序整数创建数组的另一种方法是通过 .keys(),它也将漏洞看作是未定义的元素:
> [... new Array(3).keys()]
[0,1,2]

.keys() 返回一个可迭代的。我们使用扩展运算符将其转换为数组。

3. 补充:创建数组

  • 填充空元素或未定义:
new Array(3)
→ [ , , ,]
Array.from({length: 2})
→ [undefined, undefined]
[...new Array(2)]
→ [undefined, undefined]
  • 填充任意值:
const a=[]; for (let i=0; i<3; i++) a.push(0);
→ [0, 0, 0]
new Array(3).fill(0)
→ [0, 0, 0]
Array.from({length: 3}, () => ({}))
→ [{}, {}, {}] (unique objects)
  • 整数范围:
Array.from({length: 3}, (x, i) => i)
→ [0, 1, 2]
const START=2, END=5; Array.from({length: END-START}, (x, i) => i+START)
→ [2, 3, 4]
[...new Array(3).keys()]
→ [0, 1, 2]

3.1 方法推荐

我更喜欢以下方法,重点是可读性,而不是性能。

  • 你需要创建一个你将完全填充的空数组,然后呢?
new Array(LEN)
  • 你需要创建一个用原始值初始化的数组吗?
new Array(LEN).fill(0)
  • 你需要创建一个用对象初始化的数组吗?
Array.from({length: LEN}, () => ({}))
  • 你需要创建一系列整数吗?
Array.from({length: END-START}, (x, i) => i+START)

如果您正在处理整数或浮点数的数组,请考虑 以此为目的创建的类型数组。它们不能有空元素,并且总是用零初始化。

提示: 数组性能通常没有那么重要

  • 在大多数情况下,我不会过分担心性能问题。即使是空元素的数组也非常快。我们可以更多得去考虑怎么让代码易于理解更有意义。

  • 此外,引擎优化的方式和位置也一直在发生变化。明天总是会比今天更快。

4. 致谢

感谢 Mathias Bynens 和 Benedikt Meurer 帮助我了解到 V8 细节。

5. 进一步阅读