阅读 517

js引擎与数组和原型访问优化

数组访问优化

数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。

js中的数组有所不同,它实际上也是一种特殊的对象,数组中元素的下标(index)是key,而元素则是value。此外数组对象还有一个额外的属性, 即:“length”。

当给数组添加元素时:

可以发现每个元素的属性结构是相同的,所以没有必要为每个下标设置属性信息

当我们自定义了数组下标的属性信息后,会发生什么?

const array = Object.defineProperty(
  [],
  '0': {
    value: 'jsconfeu',
    writable: false,
    enumerable: false,
    configurable: false,
  }
)
复制代码

这样做会破坏js引擎对数组元素属性的优化,此时js引擎不再使用Elements这种数据结构来存储元素而是整个回退到使用Dictionary Elements来表示

延伸思考:为什么vue不支持对数组下标实现响应式?

原型访问优化

function Bar(x) {
  this.x = x
}
Bar.prototype.getX = function getX() {
  return this.x
}
复制代码
const foo = new Bar(true)
// ↓↓↓↓↓↓↓↓↓↓↓↓

var obj = {}
obj.__proto__ = Bar.prototype
var result = Bar.call(obj, true)
return typeof result == 'object' ? result : obj
复制代码
  1. 创建一个空对象obj;
  2. 把obj的__proto__属性指向Bar.prototype, 此时就构成了一个: obj --> Bar.prototype --> Object.prototype -> null 原型链
  3. 在obj对象的执行空间调用Bar函数,并传入true参数, 这句执行完成后obj就包含了一个属性,值为true
  4. 考察第3步返回的返回值,如果无返回值或者返回一个非对象值,则将obj返回作为新对象;否则会将返回值作为新对象返回。

const x = foo.getX()
// ↓↓↓↓↓↓↓↓↓↓↓↓

const $getX = foo.getX
const x = $getX.call(foo)
复制代码

对于原型对象,将执行1+2n次查找(n为涉及到的原型个数)。而在js中,对原型属性的查找和使用是一个及其频繁的操作。

const anchor = document.createElement('a')

const otherAnchor = anchor.cloneNode();
复制代码

逐步优化

1. 不再将实例的prototype指针存在实例本身,而是将其直接存储在实例的隐藏类上。

这样就将1+2n次的查找降低至1+n次

2. v8引擎的特殊优化

v8引擎针对原型对象使用一个特殊的shape类型,不会与其它任何对象尤其是原型对象共享;

原型对象shape类型配合Inline cache(内联缓存)技术,加速访问

当第一次从原型链中访问到getX后,v8引擎通过内联缓存技术缓存了四个字段:

  1. offset:getX的位置偏移信息;
  2. prototype: getX存在于哪一个原型上;
  3. shape:实例对象的shape类型;
  4. ValidityCell: 指向实例对象直接连接的原型对象的validityCell属性;

当后面再次调用到getX方法时,只需要先检查实例对象的shape类型中是否存在getXy以及ValidityCell参数是否依然有效,如果有效则直接从缓存的prototype中通过offset属性获取到getX。

3. 修改原型对象带来的影响

当原型对象发生改变后,原型会指向一个新的shape类型,而之前缓存的ValidityCell也会因此而失效。所以当下次再去访问getX方法时,就无法使用之前内联缓存的信息,需要“从头再来”,带来了性能上的损失。

还有更糟糕的情况❌

修改了“上游”的原型对象,会导致“下游”的ValidityCell全部失效,之前所做的缓存处理,也全部作废。

Tips

  1. 不要使用Object.defineProperty去操纵数组下标;
  2. 不要随意修改原型对象(如果一定要修改的话最好是在最开始的时候处理)
关注下面的标签,发现更多相似文章
评论