本文是系列文章第二篇,详情见第一篇。
object model用来优化对象的属性访问。
属性回顾
ecma规范将所有的对象定义为string key映射到property attributes的字典。
Shapes
当不同对象有相同的属性keys,我们称它们有相同的shape,js引擎通过shape来优化对属性的访问。 如果我们将每个对象作为一个字典保存,会存在两个问题:
- 如何高效获取property attribute
- 相同shape的对象如果全量保存带来的信息冗余
因此,js引擎会单独保存shape,其会包含属性key和除[[value]]以外的attribute,以及用offset字段表示在对应对象中的位置。这样就解决了上述两个问题
Transition chains and trees
当我们在一个现有shape基础上添加新属性时,新的shape会保存新的属性,并利用transition chains连接原来的shape。每个shape可以被多个新的shape连接,构成树状。
当对象初始时没有属性时,初始的shape就是空shape。
但当一个对象有很多属性时,沿transition chains寻找某个属性时复杂度就变成了o(n),或者再退化为字典。
Inline Caches (ICs)
ic用来缓存对应shape指定属性的offset。
Validity cells
前面主要是讨论的对象本身的属性,而js的继承是基于原型的,而[[proto]]也是一个普通的属性,因此当我们读取原型上的属性时就要沿着原型链去查询。 比如
class Bar {
constructor(x) { this.x = x; }
getX() { return this.x; }
}
const foo = new Bar(true);
const x = foo.getX();
读取getX时就需要三步
- 判断getX是否在本身
- 查找原型
- 判断getX是否在原型
即总的查找不熟需要1+2N,N是原型链上的原型数。
如果我们不保存原型对象本身,而是保存原型对象的shape,那么复杂度就会降为1+N,并在此基础上添加了inline cache。
每个原型的shape在这里都认为是独一无二的,不和其他对象共用, 并关联一个ValidityCell来表示原型对象本身或原型链上层是否修改
这意味着如果Object.prototype有修改,所有缓存都会无效,修改原型链本身就是不好的行为,除非在其他代码执行之前修改
当下一次查找时,如果cache命中,且ValidityCell有效,就可以直接在对应shape指定的offset读取。
array相关
其中array是一种特殊的对象,有自动更新的属性length。其index是有效范围0 to 2³²−2的数字,对应元素用对应字符串为key的属性表示,比如第0个属性的key为'0'。 由于每个index属性的attribute默认writable, enumerable, and configurable,因此就不会保存这些attribute信息,并且区别于其他命名属性保存。如果手动修改了某个元素的attribute,js引擎就需要使用字典另外保存这个元素,因此不要这么做。