原文 : https://www.yuque.com/wmaoshu/blog/sbss26
本篇博客从 v8 引擎角度透析 JavaScript 中对象的本质,以及 v8 引擎是怎么借鉴编译型语言的某些特性(比如 结构体、地址偏移、预解析、内联缓存 等)来优化对象属性的访问性能的, 这也从另一个角度说明了项目使用 typescript 必要性的原因。
在 javascript 语言中,任何的对象都是由属性名称和属性值两部分组成,对于属性名称有字符串类型和数字类型,对于属性值来说可以是任意的类型。或者你会听说 JavaScript 中的对象又称为字典,以键值对的方式存储和获取。这些是我们一开始学习 JavaScript 的认知,这就抛出了第一个问题:
字典的查找属性是非线性的,和编译语言在编译阶段就会将变量替换成偏移量或者直接寻址获取到值相比是非常慢的,那如何优化动态语言的查找属性的方式使得性能逼近与编译语言呢?
要回答这个问题我们要从 v8 对于对象的内部表示开始说起,然后分别对 命名属性、元素属性、隐藏类、in-object 进行介绍。最终介绍函数中如何借鉴对象的隐藏类来实现内联缓存来优化函数性能的。
结构与特性
V8 解析
安装jsvu
- 本地预备安装 node、npm。
- 全局安装 jsvu: npm install jsvu -g
- 将 ~/.jsvu (如果不存在创建)路径添加到系统环境变量中:export PATH=" H O M E / . j s v u : {HOME}/.jsvu: HOME/.jsvu:{PATH}"
- 安装 v8-debug 环境: jsvu --os=mac64 --engines=v8-debug
- 在 ~ 目录下执行:.jsvu/v8-debug --allow-natives-syntax [ js 文件绝对地址 ]
执行编译
定义 index.js 文件:
const obj = {
1: 'frist',
2:'second',
first: 1,
second: 2
};
%DebugPrint(obj);
在 ~ 目录下 执行 .jsvu/v8-debug --allow-natives-syntax …/index.js
会得到如下结构信息:
DebugPrint: 0x5e308148d61: [JS_OBJECT_TYPE]
- map: 0x05e30830736d <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x05e3082c3be9 <Object map = 0x5e3083021b5>
- elements: 0x05e308148d75 <FixedArray[19]> [HOLEY_ELEMENTS]
- properties: 0x05e308042229 <FixedArray[0]> {
0x5e3080436cd: [String] in ReadOnlySpace: #first: 1 (const data field 0)
0x5e308043b4d: [String] in ReadOnlySpace: #second: 2 (const data field 1)
}
- elements: 0x05e308148d75 <FixedArray[19]> {
0: 0x05e308042429 <the_hole>
1: 0x05e3082d24f9 <String[5]: #frist>
2: 0x05e308043b4d <String[6]: #second>
3-18: 0x05e308042429 <the_hole>
}
0x5e30830736d: [Map]
- type: JS_OBJECT_TYPE
- instance size: 20
- inobject properties: 2
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x05e308307345 <Map(HOLEY_ELEMENTS)>
- prototype_validity cell: 0x05e308242445 <Cell value= 1>
- instance descriptors (own) #2: 0x05e308148de5 <DescriptorArray[2]>
- prototype: 0x05e3082c3be9 <Object map = 0x5e3083021b5>
- constructor: 0x05e3082c3821 <JSFunction Object (sfi = 0x5e308248cd1)>
- dependent code: 0x05e3080421b5 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
可以看出,对于 obj 对象来说经过 v8 编译之后生成了带有如下属性的结构:
- map:隐藏类或者我们称之为 hiddenClass。用来描述该对象的一些结构的性质,类似于编译语言中的结构体。但隐藏类仅仅与命名属性有关,与元素属性没有关系。在隐藏类中会存在如下几个属性:
- instance size:该对象实例的大小,这里就类似于 c语言中一个结构体所占内存的大小是一样的。这个属性在初始化对象的时候就声明创建,从而决定 命名属性存储结构是否是线性的,是快访问还是慢访问。
- inobject properties:in-object 属性的个数,in-object是一种优化属性快速访问的手段。
- back pointer:在对象的属性进行增加删除操作的时候会生成 隐藏类链表,用来维护链表的链接关系。
- instance descriptors:指向描述符数组的指针,这里面维护者命名属性的信息,如名称本身和存储值的位置,当在快访问时以便于能够快速定位命名属性的位置。
- prototype:对象的原型属性。
- prototype: 对象的原型
- elements:该对象的元素属性,元素属性定义为属性名是数字的属性,这里的数字也有可能是数字字符串。一般 elements的结构是数组, 但在手动添加属性的时候会退化成字典。比如 obj 对象中的 1 2 属性。
- properties:该对象的命名属性,命名属性定义为属性名称是字符串的属性,一般 properties 属性的结构是数组和字典,如果是数组多半在隐藏类中 instance descriptors 中有相应属性值的存储位置信息,可以根据存储位置 通过偏移量直接获取到。比如 obj 属性中的 frist second 属性。
如上述 obj来说,elements、properties 属性都是 FixedArray 也就是数组类型,在 map 隐藏类的 instance descriptors 中存储着 frist second 命名属性的地址映射。同时 frist second 两个命名属性也都是 in-object 的。unused property fields 为 20 , 如果在增加超过 20 大小的属性,则 properties 会退化成 NameDictionary 也就是字典。
可以修改代码为:
const obj = {
'1': 'frist',<