v8 采用了哪些策略提升对象的访问速度

83 阅读5分钟

v8 采用了哪些策略提升对象的访问速度

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情

从一个”坑“讲起

后端返回了一组数据,从最近日期往后根据年份返回数据

{
    2022: '疫情1年',
    2021: '疫情2年',
    2020: '疫情3年',
    2019: '疫情4年',
}

根据我我的习惯,直接遍历,渲染就OK了吧,然而...

let obj = {
    2022: '疫情1年',
    2021: '疫情2年',
    2020: '疫情3年',
    2019: '疫情4年',
}
for (i in obj ) {
    console.log( `${i}`,obj[i])
}
// 2019 疫情4年
// 2020 疫情3年
// 2021 疫情2年
// 2022 疫情1年

疑? 返回的明明是2022,2021,2020,2019 。为什么遍历出来顺序就变了呢?让我们来一探究竟

排序属性和常规属性

根据上面的现象,我们可以得出以下结论

  • 设置的数字属性,按照升序属性排列输出

那如果夹杂着字符串,或者字符串和数字一起出现,会怎么输出呢?


let obj = {
   'todo2': '做核酸',
   'todo1': '戴口罩',
    2021: '疫情3年',
    2020: '疫情2年',
    2022: '疫情4年',
    2019: '疫情1年',
}
for (i in obj ) {
    console.log( `索引${i}`,obj[i])
}
// 2019 疫情1年
// 2020 疫情2年
// 2021 疫情3年
// 2022 疫情4年
// todo2 做核酸
// todo1 戴口罩

根据观察,我们可以得出以下结论

  • 设置的数字属性最先被打印出来,并且按照索引值大小进行升序排列
  • 字符串根据创建时的顺序进行排列
  • 并且数字属性的排序优于字符串属性排序

上述的总结,也是标准里的规定,那么为什么要这样做呢?

提高访问速度

访问速度,我们首选数组,因为是连续存储的线性结构,根据索引即可轻松访问到元素。对于对象,我们总是说他是键和值的集合,是一个字典结构(非线性存储)。然后在v8里,为了提升访问速度,给了对象同样的快的访问速度。v8会对属性进行分类,对于数字属性,也会按照顺序去存储,叫排序属性(elements),对于常规字符串,按照创建顺序去存储,叫做常规属性(properties)

image.png

通过上图,我们发现对象包含了两个隐藏属性。其中element属性指向了element对象,在element对象中,会按照顺序存放排序属性。properties属性指向了properties对象,会按照创建时的顺序存放常规属性

分解成了两种数据结构后,如果执行索引操作,v8会先从element对象中按顺序读取所有元素,然后在properties对象中读取所有元素,这样就完成了一次索引操作

排序属性

在对象中,属性是数字的,或者是字符串数字统称为排序属性。因其是线性结构,根据索引进行查找,所以也称之为快属性。

常规属性

在对象中,属性是字符串的,称为排序属性。因其是非线性结构的字典,根据键进行查找,所以也称之为慢属性。

对象内属性

将不同的属性分别保存在properties和elements,无疑简化了程序的复杂度。但是在查找元素时,需要使用'.'操作符,无疑是多了一步操作。v8采取了一个权衡策略,就是将部分常规属性直接保存至对象本身,我们称之为对象内属性(in-object-properties)

image.png

存储的转换

如果仅仅是访问,上述的策略可能没什么问题,但是如果要增加,要删除大量的数据,线性结构还使用嘛? 我们来看大量数据的增加,删除之后,数据结构在内存快照是如何存放的

function Foo(property_num, element_num) {
		// 添加常规属性
		for (let i = 0; i < property_num; i++) {
			let ppt = `property_num${i}` 
			this[ppt] = ppt;
		}
		// 添加排序属性
		for (let i = 0; i < element_num; i++) {
			this[''+i] = `element_num${i}`;
		}
	}
  1. 创建10个常规属性,10个排序属性
var bar1 = new Foo(10, 10);

image.png

这时候属性的内存布局是这样的

  • 10个排序属性在elements中
  • 10个常规属性,作为对象内属性存放在bar1函数内部
  1. 创建20个常规属性,10个排序属性
var bar2 = new Foo(20, 10);

image.png

这时候属性的内存布局是这样的

  • 10个排序属性在elements中
  • 10个常规属性,以线性结构方式存放在properties
  • 10个属性直接存放在bar2对象内部
  1. 创建100个常规属性,10个排序属性
var bar3 = new Foo(100, 10);

image.png

这时候属性的内存布局是这样的

  • 10个排序属性在elements中
  • 90个常规属性,以非线性结构方式存放在properties
  • 10个属性直接存放在bar3对象内部

"坑"讲解

文章开头说的踩坑,其实就是v8为了提升对象的访问速度,会将键进行分类存储,导致后端返回的数据和v8解析的不一致,下面来聊聊解决方案

  1. 既然对象不能保证顺序,对于线性结构,直接使用数组返回不就好了嘛
  2. 如果非要返回对象的键是数字,可以利用Object.entries()将对象转为数组,再进行处理,这样顺序也不会乱
  3. 数字欺骗,将数字加上额外的字符,等待处理完毕,再将数据还原

知识总结

  1. 数组访问之所以快,是因为本身是线性结构。如果想提升对象的访问速度,需要对键进行分类,根据键是数字分为排序属性(elements)和键是字符串分为常规属性(properties)
  2. 常规属性,要通过 '.' 操作符读取,为了简化操作,在少于10个时,直接将属性存储在对象内部,称之为对象内属性
  3. 属性少的时候,常规属性内部也是线性结构存储,但是当属性多,并且频繁操作时,v8会更换策略,将常规属性内的线性结构降级为非线性结构,也就是字典进行进行存储
  4. 通过截图,我们会发现还有一个__proto_(原型),和 map
  5. map 是一个隐藏类,是从空间和时间维度去提升对象的访问速度,下一篇聊聊map这个隐藏类