原文链接:
创建数组的最佳方式是使用字面量:
const arr = [0,0,0];复制代码
但也不总是最佳方案,例如在创建大型数组时。这篇博客就探讨了在这些情况下该怎么做。
1. 没有空元素的数组往往表现更好
在大多数编程语言中,数组是连续的值序列。 在 JavaScript
中,Array
是一个将索引映射到元素的字典,它可以有空元素,而且空元素也有自己对应的索引,但它并不映射到元素上。例如下面这个数组就有一个空元素在索引 1 的位置上。
> Object.keys(['a',, 'c'])[ '0', '2' ]复制代码
没有空元素的数组称作密数组,密数组往往表现更好,因为它们可以连续(内部)存储。一旦至少有一个空元素,内部表示必须改变。有两种选择:
- 字典,查找需要更多时间并且存储开销更大。
- 连续的数据结构,具有空元素的标记值。检查值是否是一个空元素,需要额外的时间。在任何一种情况下,如果引擎遇到一个空元素,它不能只返回
undefined
,而必须遍历原型链并搜索一个名称为空元素索引的属性,这需要花费更多时间。
在某些引擎中,例如 V8
,切换到性能较低的数据结构是永久性的。即使所有空元素都添加上值,它们也不会切换回来。
有关 V8
如何表现数组的更多信息,请参阅 Mathias Bynens 的 。
2. 创建数组
数组构造函数
使用数组构造函数来创建一个给定长度的数组是一个很普遍的事情。
const LEN = 3;const arr = new Array(LEN);assert.equal(arr.length, LEN);// arr only has holes in itassert.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;shuconst 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. 进一步阅读
- “探索ES6”中的“”一章
- “探索ES6”中的“”