前段时间任务有点繁忙,好久也没去学点啥,这两天任务暂时告一段落,看到隐藏类有关的东西感觉很有意思,做个笔记。。
什么是隐藏类
Javascript作为动态语言,在对象创建以后也可以新增或者删除属性,平时用起来还是很方便的,例如:
function Aaa() {
this.name = 'aa'
this.age = 20
}
const a = new Aaa()
a.hobby = '吃饭'
delete a.age
console.log(a) // {name: "aa", hobby: "吃饭"}
大多数 Javascript 解释器使用类似字典的对象(基于哈希函数)来存储对象属性值在内存中的位置。这种结构使得在 Javascript 中检索属性的值计算成本更高,而为了加快对象属性和方法在内存中的查找速度,V8引擎引入了隐藏类(Hidden Class)的机制。
在初始化对象的时候,V8引擎会指向一个隐藏类,随后在程序运行过程中每次增减属性,就会创建一个新的隐藏类或者查找之前已经创建好的隐藏类。每个隐藏类都会记录对应属性在内存中的偏移量,从而在后续再次调用的时候能更快地定位到其位置,隐藏类的目的是优化属性访问时间。
图解隐藏类
例如:
function Point(x,y) {
this.x = x;
this.y = y;
}
var obj = new Point(1,2);
当声明了Point函数,Javascript 将创建隐藏类 C0,这时C0中没有属性
当执行了this.x = x
V8 将创建第二个隐藏类,称为 C1,它基于 C0。C1 描述了内存中可以找到属性 x 的位置。在这种情况下,x 存储在偏移量0 处,同时会更新 C0,会在C0中指出如果添加x属性,隐藏类应该切换到 C1,现在的下面point对象的隐藏类现在是 C1。
当执行this.y = y
,重复此过程。创建一个名为 C2 的新隐藏类,向 C1 添加说明:如果添加y属性(已包含属性“x”),则隐藏类应更改为 C2,并且point对象隐藏类更新为C2
注意:隐藏的类转换取决于将属性添加到对象的顺序,下面这种方式创建的两个对象隐藏类不同
1 function Point(x,y) {
2 this.x = x;
3 this.y = y;
4 }
5
7 var obj1 = new Point(1,2);
8 var obj2 = new Point(3,4);
9
10 obj1.a = 5;
11 obj1.b = 10;
12
13 obj2.b = 10;
14 obj2.a = 5;
直到第 9 行,obj1 和 obj2 共享同一个隐藏类。但是,由于属性 a 和 b 以相反的顺序添加,因此 obj1 和 obj2 由于遵循不同的转换路径而最终具有不同的隐藏类。
这里说下如何在Chrome中观察隐藏类,下述的例子都会在在控制台看是否为同一个隐藏类
常规属性相关
当给对象添加删除常规属性的时候隐藏类都会改变比如:
function Aaa() {
this.name = 'aa'
this.age = 20
}
const a1 = new Aaa()
const a2 = new Aaa()
const a3 = new Aaa()
a3.hobby = '吃饭'
- 按照不同顺序添加属性例子:
function Aaa() {
this.name = 'aa'
this.age = 20
}
const a1 = new Aaa()
const a2 = new Aaa()
const a3 = new Aaa()
a1.hobby = '上课'
a1.hobby2 = '学习'
a2.hobby = '吃饭'
a2.hobby2 = '睡觉'
a3.hobby2 = '玩游戏'
a3.hobby = '看电影'
排序属性相关
这上面说的是常规属性age,name之类的,如果是排序属性会和上面的不同
先说下什么是排序属性
关于排序属性可以看下这篇文章 https://juejin.cn/post/6844903646614781966
这里只说下结论:
- 如果属性名的类型是
Number
,那么Object.keys
返回值是按照key
从小到大排序 - 如果属性名的类型是
String
,那么Object.keys
返回值是按照属性被创建的时间升序排序。
例如:
const obj = {
10: '10',
11: '11',
20: '20',
15: '15'
}
Object.keys(obj).forEach(k => {
console.log(k) // 10 11 15 20
})
for(const k in obj) {
console.log(k) // 10 11 15 20
}
这种属性其实是排序属性,上面的结论中说的如果属性名是Number类型的话,会从小到达排序,但其实最终对象的属性名是string类型,可以打印一下
Object.keys(obj).forEach(k => {
console.log(typeof k) // string
})
这里可以认为能正常被Number()转换的是按照从小到大排序的就行
新增排序属性时的隐藏类如下:
function Bbb() {
this[2020] = 'bb'
this[2021] = 18
}
const b1 = new Bbb()
const b2 = new Bbb()
const b3 = new Bbb()
b3[2022] = '睡觉'
从图中可以看出排序属性的新增或删除是不会影响隐藏类的
内联缓存
这里仅拥有隐藏类其实还不够,毕竟引擎在执行过程中还需要查找隐藏类。为了取得更好的性能,V8引擎加入了内联缓存(Inline Caching)技术来优化运行时查找对象及其属性的过程
内联缓存:每当在特定对象上调用方法时,V8 引擎都必须查找该对象的隐藏类,以确定访问特定属性的偏移量。在对同一个隐藏类两次成功调用同一个方法后,V8 省略了隐藏类查找,只是简单地将属性的偏移量添加到对象指针本身。对于该方法的所有未来调用,V8 引擎假定隐藏类没有改变,并使用先前查找存储的偏移量直接跳转到特定属性的内存地址,这会大大提高执行速度。
内联缓存也是为什么相同类型的对象共享隐藏类如此重要的原因。如果你创建了两个相同类型但隐藏类不同的对象(就像我们在前面的例子中所做的那样),V8 将无法使用内联缓存,因为即使这两个对象是相同类型的,它们对应的隐藏类为其属性分配不同的偏移量,最终会导致性能有所下降
最后的结论
so,在平时开发过程中,应该尽量保证属性初始化的顺序一致,这样生成的隐藏类可以得到共享。
同时,尽量在构造函数里就初始化所有对象成员,减少后续新增删除属性,减少隐藏类的产生。
参考资料
https://juejin.cn/post/6972702293636415519
https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf
https://richardartoul.github.io/jekyll/update/2015/04/26/hidden-classes.html