对象属性查找过程中的产物-隐藏类

本文介绍了JavaScript中的隐藏类和内联缓存机制,这两种技术是V8引擎优化对象属性访问的关键。隐藏类通过记录属性在内存中的偏移量提高查找速度,而内联缓存进一步加速了属性访问。文章通过实例展示了属性添加顺序如何影响隐藏类,并建议开发者在初始化对象时保持属性顺序的一致性,以优化性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前段时间任务有点繁忙,好久也没去学点啥,这两天任务暂时告一段落,看到隐藏类有关的东西感觉很有意思,做个笔记。。

什么是隐藏类

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

这里只说下结论:

  1. 如果属性名的类型是Number,那么Object.keys返回值是按照key从小到大排序
  2. 如果属性名的类型是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-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code-ac089e62b12e

https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf

https://richardartoul.github.io/jekyll/update/2015/04/26/hidden-classes.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值