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)
通过上图,我们发现对象包含了两个隐藏属性。其中element属性指向了element对象,在element对象中,会按照顺序存放排序属性。properties属性指向了properties对象,会按照创建时的顺序存放常规属性
分解成了两种数据结构后,如果执行索引操作,v8会先从element对象中按顺序读取所有元素,然后在properties对象中读取所有元素,这样就完成了一次索引操作
排序属性
在对象中,属性是数字的,或者是字符串数字统称为排序属性。因其是线性结构,根据索引进行查找,所以也称之为快属性。
常规属性
在对象中,属性是字符串的,称为排序属性。因其是非线性结构的字典,根据键进行查找,所以也称之为慢属性。
对象内属性
将不同的属性分别保存在properties和elements,无疑简化了程序的复杂度。但是在查找元素时,需要使用'.'操作符,无疑是多了一步操作。v8采取了一个权衡策略,就是将部分常规属性直接保存至对象本身,我们称之为对象内属性(in-object-properties)
存储的转换
如果仅仅是访问,上述的策略可能没什么问题,但是如果要增加,要删除大量的数据,线性结构还使用嘛? 我们来看大量数据的增加,删除之后,数据结构在内存快照是如何存放的
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}`;
}
}
- 创建10个常规属性,10个排序属性
var bar1 = new Foo(10, 10);
这时候属性的内存布局是这样的
- 10个排序属性在elements中
- 10个常规属性,作为对象内属性存放在bar1函数内部
- 创建20个常规属性,10个排序属性
var bar2 = new Foo(20, 10);
这时候属性的内存布局是这样的
- 10个排序属性在elements中
- 10个常规属性,以线性结构方式存放在properties
- 10个属性直接存放在bar2对象内部
- 创建100个常规属性,10个排序属性
var bar3 = new Foo(100, 10);
这时候属性的内存布局是这样的
- 10个排序属性在elements中
- 90个常规属性,以非线性结构方式存放在properties
- 10个属性直接存放在bar3对象内部
"坑"讲解
文章开头说的踩坑,其实就是v8为了提升对象的访问速度,会将键进行分类存储,导致后端返回的数据和v8解析的不一致,下面来聊聊解决方案
- 既然对象不能保证顺序,对于线性结构,直接使用数组返回不就好了嘛
- 如果非要返回对象的键是数字,可以利用Object.entries()将对象转为数组,再进行处理,这样顺序也不会乱
- 数字欺骗,将数字加上额外的字符,等待处理完毕,再将数据还原
知识总结
- 数组访问之所以快,是因为本身是线性结构。如果想提升对象的访问速度,需要对键进行分类,根据键是数字分为排序属性(elements)和键是字符串分为常规属性(properties)
- 常规属性,要通过 '.' 操作符读取,为了简化操作,在少于10个时,直接将属性存储在对象内部,称之为对象内属性
- 属性少的时候,常规属性内部也是线性结构存储,但是当属性多,并且频繁操作时,v8会更换策略,将常规属性内的线性结构降级为非线性结构,也就是字典进行进行存储
- 通过截图,我们会发现还有一个__proto_(原型),和 map
- map 是一个隐藏类,是从空间和时间维度去提升对象的访问速度,下一篇聊聊map这个隐藏类