创建一个组数最佳的方式是使用字面量:
const arr = [0, 0, 0];
但是在某些场景下(如创建大型数组)是不适合选择字面量的方式的,这篇文章将会告诉你在这些场景中应该如何处理。
1. 没有空索引的数组表现更好
在大多数编程语言中,数组是由连续的值组成的序列。在 JavaScript 中,数组是索引到元素的字典。它可能有“洞”--在零到数组长度之间的索引中,那些没有映射到元素的索引(空索引)。举个例子,下面的数组的索引 1 就是空索引:
> Object.keys(['a',, 'c'])
[ '0', '2' ]
没有空索引的数组也被 称为 密集数组(dense)或满数组(packed)。密集数组会有 更好的表现,因为他们可以被连续存储。一旦数组中有一个空索引,数组的内部表示就需要改变。这里有两个选择:
使用字典,需要更多的查找时间并且存储开销更大
使用有空索标记的连续数据结构,需要话费额外的时间到空索引检测上
如果解释器遇到一个空索引,它都不能只返回 undefined,它会遍历原型链并找到一个属性名和这个空索引的索引值相同的属性,这又会花费更多时间。
在某些引擎中,例如 V8,切换到低性能的数据结构是永久性的。就算所有的空索引都被去掉了,它们也不会切换回来。
有关 V8 中数组的表示请阅读 Mathias Bynens 写的Elements kinds in V8。
2. 创建数组
2.1 构造函数
通常创建一个指定长度的数组会使用数组构造函数:
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方法会改变现有的数组,并使用指定的值填充它。在使用new Array()初始化数组是有帮助的:
const LEN = 3;
const arr = new Array(LEN).fill(0);
assert.deepEqual(arr, [0, 0, 0]);
警告:如果你用对象填充数组,那么所有的元素都是引用自同一个对象实例:
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;
const arr = [];
for (let i=0; i < LEN; i++) {
arr.push(0);
}
assert.deepEqual(arr, [0, 0, 0]);
2.4 填充undefied
Array.from可以讲类数组值转换成数组,它将空索引指向undefied值。我们可以使用它将每个数组位置填充为undefied:
> Array.from({length: 3})
[ undefined, undefined, undefined ]
参数{length:3}表示数组长度为3,也可以使用new Array(3),但是用Array.from(new Array())来创建数组,仍然会得到一个稀疏数组。
展开运算符也能起到类似的作用:
> [...new Array(3)]
[ undefined, undefined, undefined ]
2.5 遍历Array.from
如果提供遍历函数作为其第二个参数,就可以使用Array.from的遍历功能。
1. 用整数填充数组
> Array.from({length: 3}, () => 0)
[ 0, 0, 0 ]
2. 用对象填充数组
> Array.from({length: 3}, () => ({}))
[ {}, {}, {} ]
3. 自增长整数填充数组
> Array.from({length: 3}, (x, i) => i)
[ 0, 1, 2 ]
4. 任意范围整数填充数组
> 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. 补充
总结一下前面提到的方法:
空索引或 undefied 填充数组
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 推荐的使用方式
我喜欢以下的数组使用方法,我的重点在可读性,而不是性能。
1. 当您需要创建一个稍后才填充数据的空数组,
new Array(LEN);
2. 当您需要创建一个有初始值的数组
new Array(LEN).fill(0);
3. 当您需要用对象初始数组
Array.from({ length: LEN }, () => ({}));
4. 当您需要用连续的整数创建数组
Array.from({ length: END - START }, (x, i) => i + START);
如果您需要处理整数或浮点数的数组,请参考Typed Arrays。它们不能有空索引,并且最好用 0 初始化。
提示:数组性能通常无关紧要
对于大多数情况,我不会台担心性能问题。即使是带有空索引的数组也非常快。担心您的代码的易读性会更有意义。
除此之外,引擎优化的方式也在发生着变化,现在最快的数组使用方式,可能明天就没有优势了。
4. 感谢
感谢 Mathias Bynens 和 Benedikt Meurer 帮助我正确地获取 V8 细节。
5. 延伸阅读
“Exploring ES6” 的 “Typed Arrays"章节
“Exploring ES6” 的 “ES6 and holes in Arrays"章节