性包,并不基于连续存储。而密集数组为连续存储,无空洞,但初始化时需付出多次扩容代价。
进行数组操作(如 pop、push等),密集数组比空洞数组效率更高。
空洞数组的操作,需在在原型链上进行额外检查和昂贵查找。
如果引擎遇到一个空洞,它不能只返回 undefined,必须查找原型链并搜索一个名称为“空洞索引”的属性,这需要花费更多时间。
所以,「前言」中的问题1,其答案跃然纸上。
问题1答案: 空洞数组的数组操作慢,因要处理额外的空洞逻辑。空洞数组要处理空洞逻辑,就算数组实际没有空洞,至少付出了「检查一下是不是空洞」的成本。
目前为止,我们看到的每种基本元素类型(即 SMI、DOUBLE 和 常规元素)都有两种风格:packed 、 holey。
元素种类转换,除了上述提及的特定到更一般,此处新增packed到其holey对应物。
四.元素种类格子
V8将上述元素种类转换,实现为lattice,绘图如下:
只能通过格子只能向下转换,转换不可逆。
最开始创建一个空洞数组,哪怕最终将空洞全部填充,其数组元素种类仍为 空洞数组。
在元素种类格子中,上层代表的元素种类比下层更具体,即可实现更细粒度的优化。
所以,越往下,其代表的元素种类越宽泛,其对象操作越慢。
为获得最佳性能,尽可能避免元素种类向下转换。
-0、 NaN 、 Infinity。它们被表示为双精度,因此添加一个 NaN 或 Infinity 会将 SMIELEMENTS 转换为 PACKEDDOUBLE_ELEMENTS
五.性能提示
大多数情况下,数组种类追踪是引擎层面的事情,不要过分担心性能。而且引擎优化的方式是不断改变的,今天最优性能方案,明天可能就不是了。
即使是空洞数组也很快,所以相比于吹毛求疵地追求性能,我们更应专注于代码可阅读性。
1.避免数组越界访问
在问题3中,数组索引 1000 超出范围,数组本身不存在该属性,因此 JS 引擎必须执行原型链查找。
一旦遇到越界情况,V8会对其进行标记,它再也不会像读取越界之前那样快。
问题2答案: 数组越界访问,需查找原型链,故速度更慢。
问题3答案: 数组越界访问会被V8标记「需处理特殊情况」,即沿原型链查找,速度更慢。
for-of、forEach、老式for循环 ,三者的性能差不多。
最后一次迭代读取超出了数组的长度, V8 的边界检查失败,检查属性是否存在失败,然后 V8 需要查找原型链。
2.避免元素种类转换
一般来说,如果需要对数组执行大量操作,请使用具体的元素种类,以便 V8 优化这些操作。
如果对整数数组执行大量操作,请避免使用-0、NaN、Infinity。
const aa1 = [3, 2, 1, +0];
PACKED_SMI_ELEMENTS
aa1.push(-0);
PACKED_DOUBLE_ELEMENTS
const aa2 = [3, 2, 1];
PACKED_SMI_ELEMENTS
aa2.push(NaN, Infinity);
// PACKED_DOUBLE_ELEMENTS
3.避免使用类数组对象
类数组对象缺少数组方法,若要使用数组方法,则需进行call绑定。
一般来说,尽可能避免使用类似数组的对象,而是使用适当的数组。
4.避免多态性
处理许多不同元素种类的数组的代码,它可能导致多态操作比仅对单个元素类型进行操作的代码版本慢。
这里的单态和多态涉及到隐藏类,可看一下V8 编译与优化。
5.避免创建空洞
一旦数组被标记为空洞,它就会永远保持空洞,即使后面所有的空洞都被