深入了解对象的属性

257 阅读6分钟

对象的常规属性与排序属性

常规属性

  • 属性键值为字符串的属性
  • 特点:根据创建时的顺序排序
const bar = {}

bar.prop2 = 'prop2'
bar.prop1 = 'prop1'
bar.prop3 = 'prop3'

for(let key in bar) {
    console.log(key)
}
// prop2
// prop1
// prop3

排序属性

  • 属性键值为数字的属性(数字字符串同样适用)
  • 特点:按照索引值大小升序排序
const bar = {}

bar['2'] = 'prop2'
bar[1] = 'prop1'
bar[3] = 'prop3'

for(let key in bar) {
    console.log(key)
}
// 1
// 2
// 3


在同时有排序属性和常规属性时的排序

  • elements 用于存放排序属性
  • properties 用于存放常规属性

image.png

V8 引擎对于对象属性访问的优化处理

当常规属性数量不超过 10 个的时候, 会直接挂载在对象上(为了提升访问速度,这些属性也被叫为内属性);但是如果超过 10 个,就会放到 properties 中, properties 用于存放常规属性。

不管是 elements 还是 properties, 当属性数量不多时, 都是以线性数据结构存储的,这样就可以直接通过下标去查找属性。但是线性数据结构有个弊端,就是如果插入或者删除属性,开销会比较大。在数据量不大的时候还好,一旦数据量很大的时候,就会放大这个弊端。这个时候V8就会采取另一种数据结构,非线性数据(字典)结构,将属性放入到字典中,这样虽然访问速度会慢一点,但是插入,删除性能更加的优异

我们把线性数据结构中的属性称呼为快属性,把非线性(字典)数据结构中的属性称为慢属性

image (1).png

那访问一个属性的顺序是什么样的呢
  1. 先去 elements 上去访问
  2. 再去 properties 上去访问

看个例子:

const obj = {}

obj.prop2 = 'prop2'
obj.prop1 = 'prop1'
obj.prop3 = 'prop3'
obj[5] = '5'
obj[7] = '7'
obj[4] = '4'
for(let key in obj) {
   console.log(key)
}
// 4
// 5
// 7
// prop2
// prop1
// prop3

需要注意的点是,elements 也不能保证一定是线程结构,当数据量超过一定后,也会变成非线性数据结构


隐藏类

什么是隐藏类

V8 会为每个对象建立隐藏类,并把隐藏类作为对象的第一个属性,当我们修改对象的时候,V8 就会以初始化的隐藏类作为基类,并新建一个隐藏类记录改变对象属性的操作和值的偏移量(class transition),最后把这个新隐藏类和目标对象进行链接。如果我们需要读取对象的值,那么只需要读取隐藏类记录的元信息,而元信息就对应着内存地址偏移量(类似于下标一样,可以直接访问),因此可以很快通过偏移量进行寻址找到内存中存储的属性值

隐藏类上存储对象的元信息,包括对象上属性的数量(准确说是快属性的数量)和对象 prototype 的引用。

在隐藏类的第三个字段,存储着对象属性数量的值和指向描述符数组的指针。描述符数组里包含:属性的名称和属性存放的内存地址。

如何创建隐藏类

默认对象每添加一个命名属性,就会创建一个新的隐藏类。属性名一样,属性名顺序也一样的对象,共享相同的隐藏类。

  • 给对象添加排序属性不会创建隐藏类。
  • 每添加一个新的属性就会创建一个新的隐藏类
  • 添加的属性顺序不一样也会创建不同的隐藏类

为什么需要隐藏类

javascript 是动态编程语言,在运行时是可以动态改变对象的结构的。在 V8 中为了实现 js 动态改变对象的特性,引入了隐藏类。有了隐藏类后,对象的新增、删除属性都会在隐藏类中实现,不会去动原对象的结构。V8 在访问对象的某个快属性时,就会先去隐藏类中找该属性的内存地址偏移量,然后 快速从内存中取出对应的属性值。

js 慢属性与快属性

js 访问快属性时, 会先访问隐藏类中元信息,寻找属性对应的偏移量,获取属性内存地址,再去 properties 找属性值

如果从一个对象中添加和删除许多属性,就会产生大量的时间和内存开销来维护描述符数组和隐藏类。因此 V8 支持慢属性,把属性值信息全部放到 properties 字典中,不在存放在隐藏类中的描述符数组中。

代码优化

  • 预先把对象的属性全部定义出来,以此减少创建新的隐藏类。 不要先定义一个空对象,之后在业务逻辑中,不停的向对象中添加新属性。
  • 减少delete操作符珊瑚对象属性,以此来减少创建新的隐藏类,可以直接将属性值设置为 null 或者 undefined

存储在 properties 中的属性可以是线性存储的,也可以是非线性字典的模式存储,前者我们可以简称为快属性(fast properties),后者则是慢属性(slow properties)。

一般情况下对象的属性都默认是快属性,但是如果频繁修改对象(包括添加属性与 delete 属性),那么就会伴随大量时间和内存开销维护隐藏类,这让隐藏类带来的性能加速显得得不偿失,因此 V8 才会引入慢属性。慢属性直接存储在单独的属性字典结构中,并且与隐藏类断开联系,在慢属性中的对象修改行为都不会更新隐藏类,因此内联缓存(inline-cache)策略也不适用于慢属性,缺点显而易见,就是读取速度很慢。

快属性线性存储在 properties 中,并且属性的所有修改都会绑定隐藏类的描述符数组。而慢属性单独使用字典结构存储,其元数据(meta data)不与隐藏类共享,因此慢属性拥有比快属性更高效的对象修改功能(添加与删除属性)但是访问元素的速度比对象内属性和快属性要慢。

总结

  1. 只有添加命名属性才能触发隐藏类,排序属性不能。
  2. 对象每添加一个新的属性就会创建新的隐藏类。
  3. 具有相同属性名且属性定义顺序相同的对象,共享相同的隐藏类。
  4. 隐藏类含有对象的属性数量和指向描述符数组的指针
  5. 快属性存放在对象的properties中,所有元信息存放在隐藏类中的描述符数组中。
  6. 在实际项目中使用对象时,把对象的属性预先全部定义出来和减少delete操作符,以此减少创建新的隐藏类。