v8中的对象结构

在某次迭代中需要给一个表格加个选择框,原型如下:

后端给的状态值如下:

javascript 复制代码
1: 已停止
2:运行中
3:正在启动
4:启动失败
5:正在停止
6:已禁用
7:休眠中
8:任务发布中

然后我的代码如下所示:

javascript 复制代码
statusMap: { 
  0: '全部服务状态',
  2: '运行中', 
  3: '正在启动', 
  4: '启动失败', 
  5: '正在停止', 
  1: '已停止', 
  7: '休眠中', 
  6: '已禁用', 
  8: '任务发布中' 
}

<v-select v-model="status"> 
  <v-option 
    v-for="(value, key) in statusMap" 
    :key="'status' + key" 
    :value="key" 
    :label="value">
  </v-option>
</v-select>

结果最终的效果图如下所示:

似乎下拉框中的选项没有按照我的代码中的顺序渲染,而是按照从小到大的顺序重排了...

什么情况?

这是因为在ECMAScript中规定:如果是数字属性,则按照从小到大的顺序排序,如果是字符串属性,则按照创建的顺序排序。 ECMAScript2015规范

那么在v8中,js中的对象是怎么存储的呢?

注:下文中用到的%DebugPrint%DebugPrintPtr都是v8-debug中的命令,v8-debug可以通过jsvu安装,jsvu可以通过npm安装

我们直接通过demo来看下

javaScript 复制代码
const obj = {}
%DebugPrint(obj)

除了我们熟悉的prototype之外,可以看到对象上有MapElementProperties等。

Elements

v8中,elements是用来存储对象的数字属性的,称为索引属性indexed properties。 索引属性会按照索引从小到大存储在elements中。测试一下:

javaScript 复制代码
const obj = {}
obj[2] = 2
obj[0] = 0
obj[1] = 1
%DebugPrint(obj)

可以看到elements上的索引属性已经排好序了。

properties

v8中,properties是用来存储对象的字符串属性的,称为命名属性Named properties。命名属性会按照创建时的顺序存储在properties中。

javaScript 复制代码
const obj = {}
obj.a = 'a'
obj.b = 'b'
%DebugPrint(obj)

似乎测试的结果没有符合我们的预期,可以看到图中的properties的长度是0,而且列举出来的命名属性ab所在的位置是in-object

解释这个问题之前,我们先设想一个问题,v8是怎么查找属性值的?比如现在想知道obj.a是多少。因为命名属性保存在properties中,所以v8要先找到properties,然后再在properties中找到a。这样无疑降低了查询效率。

为了提升性能,v8采取了将部分命名属性直接存储在对象上的策略。这种直接存储在对象上的属性就叫in-object properties

通过上面的截图我们可以看到v8预置了4in-object properties的插槽。当命名属性的个数超过4个的时候。剩下的属性会存储到properties上。我们在上面的demo中增加几行代码测试下:

javaScript 复制代码
const obj = {}
obj.a = 'a'
obj.b = 'b'
obj.c = 'c'
obj.d = 'd'
obj.e = 'e'
obj.f = 'f'
%DebugPrint(obj)

所以通过上面的测试,我们可以得出如下结论:

  1. 索引属性和命名属性是分开存储的
  2. 为了提升访问效率,对象本身会预留4in-object位置存放命名属性,剩下的命名属性会存储到properties

js中,我们创建对象一般有两种方式,一种是通过上面例子中用到的字面量的方式创建,还有一种是通过构造函数的方式创建。那么当用构造函数的方式创建对象的时候,上面的结论还存在吗?测试一下:

javaScript 复制代码
function Foo() {}
const foo = new Foo()
for (let i = 0; i < 12; i++) {
 foo[i] = i
 foo['p' + i] = 'p' + i
}
%DebugPrint(foo)

通过上图可以看出,上面的结论还是成立的。不同点就是in-object的个数变成10个了。还有一个小细节就是elements的初始长度和扩容的规则不一样,不过这又是另外一个话题了,略过。

快属性和慢属性

先看两个例子:

javaScript 复制代码
function Foo() {}
const foo = new Foo()
for (let i = 0; i < 12; i++) {
  foo['p' + 1] = 'p' + 1
}
%DebugPrint(foo)
javaScript 复制代码
function Foo() {}
const foo = new Foo()
for (let i = 0; i < 26; i++) {
  foo['p' + 1] = 'p' + 1
}
%DebugPrint(foo)

通过上面两张图可以看到,properties的数据结构不一样。当属性个数是12的时候,properties的数据结构是数组,而当属性个数是26的时候,数据结构变成字典了。这也就是我们常说的快属性和慢属性。

那什么是快慢属性呢?我们将保存在线性结构中的属性称为快属性 。因为访问速度快,通过偏移量就可以访问到。但是如果要对线性结构进行增加或者删除操作,那么效率则会低下。所以在属性过多的时候(经测试,大于25个),v8会采用非线性结构来存储属性,保存在非线性结构中的属性就称为慢属性

在日常编码中,我们经常使用的delete操作(非末尾元素)也会导致对象的属性从快属性变成慢属性。

javaScript 复制代码
function Foo() {};
const foo = new Foo();
for (let i = 0; i < 12; i++) {
 foo['p' + i] = 'p' + i;
}
delete foo.p1;
foo.p1 = 'p1';
%DebugPrint(foo);

关于v8中的快属性的更多细节可以阅读Fast properties in V8

既然properties有快属性之分,那么elements呢?那肯定也是有的。在上面所有的例子中elements的数据结构都是FixedArray。也就是线性存储的。那什么情况下elements会变成非线性存储呢?

在回答这个问题之前,我们先看下一个概念HOLEY_ELEMENTS。 还是先来两个demo

javaScript 复制代码
const arr1 = ['a', 'b', 'c'];
const arr2 = ['a', 'b', 'c'];
arr2[5] = 'e';
%DebugPrint(arr1);
%DebugPrint(arr2);

这样一看是不是比较好理解,如果数组中有元素等于the_hole_value(也就是我们说的empty),那么就是HOLEY_ElEMENTS(稀疏数组),如果数组中没有empty元素,则是PACKED_ELEMENTS(密集数组)。根据元素值的类型还可以细分出其它种类的HOLEY_ELEMENTS,比如PACKED_SMI_ELEMENTS(元素的值都是整数)等,PACKED_ELEMENTS同理。但是和本文主题无关,同样略过,对此感兴趣的可以查看Elements kinds in V8

现在回到上面那个问题,什么时候elements会非线性存储呢?和properties类似,当elements有大量(经测试,大量是指不小于1024hole的时候。elements会变成非线性结构。可以通过下面的demo验证下。

javaScript 复制代码
const obj = {};
obj[1023] = 0;
%DebugPrint(obj);
javaScript 复制代码
const obj = {};
obj[1023] = 0;
%DebugPrint(obj);
javaScript 复制代码
// 测试下是不是有hole的元素个数>=1024个从线性变非线性
const obj = {};
obj[1] = 0;
obj[1023] = 0;
%DebugPrint(obj);

tips: 所谓的快慢属性只是我们的习惯叫法,并没有优劣之分,它们有各自使用的场景,搬运v8某个开发者的一段话:

更多细节可以点击这里

Map

什么是隐藏类

v8中,每个对象的第一个属性就是自己的隐藏类map。其实在之前的例子中我们已经见过隐藏类了,就是下图红色框框的内容。(注意下蓝色框框的内容instance descriptors,这是隐藏类中一个比较重要的属性)

为什么需要隐藏类

先来几行代码

javaScript 复制代码
const obj = {
  name: 'lily',
  age: 10
}
console.log(obj.name)

当执行obj.name的时候,v8会怎么查找?因为javaScript是动态语言,也就是说对象的属性是可以随意增减的,所以当执行obj.name的时候,v8obj上有没有name都不知道,就不要说obj.name的值了。为了找到obj.name的属性值,v8需要先在in-object properties上查找是否有name,没找到则去properties上找,还没找到则需要顺着原型链去找。可以看到这整个的过程是很耗时的。追求高性能的v8肯定不会用这样的方式查找对象的属性值。所以就借鉴了静态语言的类和结构体的概念引入了隐藏类的策略。下面我们来看下隐藏类是怎么提升查找对象属性值的?

还是上面的obj。我们先用%DebugPrint(obj)看下objmap地址,然后查看下map上的instance descriptors中的详细内容:

有了map之后,当执行obj.name的时候,v8会直接在objmapinstance descriptors中找到name属性相对于obj的偏移量,根据obj的内存地址加上name的偏移量0就得到了name属性在内存中的地址了,此时就可以直接取值了,是不是快多了?

可以这么说:静态语言根据类或者结构体生成对象,而v8根据对象生成隐藏类

但是如果每个对象都产生一个隐藏类的话,那么时间和内存开销也是两个大问题,所以就出现复用隐藏类了,那么什么情况下会复用隐藏类呢?要符合三个条件:属性的名称、顺序和个数要完全相同 。用下面的demo测试下:

javaScript 复制代码
const obj1 = { name: '小红', age: 10 };
const obj2 = { name: '小米', age: 11 };
const obj3 = { name: '小花' };
const obj4 = { age: 10, name: '小草' };
%DebugPrint(obj1);
%DebugPrint(obj2);
%DebugPrint(obj3);
%DebugPrint(obj4);

从截图可以看到,只有obj1obj2map是同一个。

v8引入隐藏类是基于一个假设:对象创建出来形状就固定了,即属性不会新增或者删除。但事实是js是动态语言,对象的属性可以随意增减,那现在我们来看下对象属性增减的时候隐藏类如何变化?

注意下面这个demo截图中mapback pointer的变化~

javaScript 复制代码
// 先初始化一个obj
const obj = {}
javaScript 复制代码
obj.name = '小红'
javaScript 复制代码
obj.age = 10

从上面三张图可以得出的结论就是:对象每添加一个新属性,就会产生一个新的隐藏类,而且新的隐藏类中的 back pointer 总是指向旧的隐藏类。上面例子中三个隐藏类(按照出现的先后顺序分别命名为map0map1map2)的关系如下图所示:

其实上面的链条就是v8中的转换树transition tree。它的作用是当以相同的顺序添加相同的属性的时候能确保最后得到相同的隐藏类。来测试一下:

新加一行代码obj1 = {},如下图所示:

要给obj1来个name属性呢?

但是此时给obj1来个不一样的属性呢?比如phone

此时可以隐藏类之间的转换树如下:

现在来看看删除属性的情况,为了方便观察删除属性时map如何变化,我们先给obj增加一个height属性。

javaScript 复制代码
obj.height = 150

运行到上面这行代码的时候,此时内存中的各对象之间的关系如下所示:

好了,开始删除了,我们先删除最后一个属性height

发现了什么?objmap退回到了0x02410015ad75上了

再删一个看看,这次我们删除非末尾元素 name

是不是发生了点不一样的东西?是的,如果删除的属性不是最后一个属性的话,那么v8会不再维护map之间的转化关系,而是转成非线性结构存储了。

日常编码建议

  • 初始化属性相同的对象时,保证属性的顺序保持一致
  • 尽量一次性初始化完属性对象
  • 避免使用delete

说完了v8中的对象表达,现在回到最开始的那个问题,那怎么还原原型上的顺序:

javascript 复制代码
<v-select v-model="status"> 
  <v-option label="全部服务状态" :value="0"></v-option>
  <v-option label="运行中" :value="2"></v-option>
  <v-option label="正在启动" :value="3"></v-option> 
  <v-option label="启动失败" :value="4"></v-option> 
  <v-option label="正在停止" :value="5"></v-option> 
  <v-option label="已停止" :value="1"></v-option> 
  <v-option label="休眠中" :value="7"></v-option> 
  <v-option label="已禁用" :value="6"></v-option> 
  <v-option label="任务发布中" :value="8"></v-option> 
</v-select>
javaScript 复制代码
statusMap: new Map([ 
  [0, '全部服务状态'], 
  [2, '运行中'], 
  [3, '正在启动'], 
  [4, '启动失败'], 
  [5, '正在停止'], 
  [1, '已停止'], 
  [7, '休眠中'], 
  [6, '已禁用'], 
  [8, '任务发布中'] 
]),

<v-select v-model="status"> 
  <v-option 
    v-for="[key, value] in statusMap" 
    :key="'status' + key" 
    :value="key" 
    :label="value">
  </v-option>
</v-select>

参考资料

相关推荐
Lupino24 分钟前
被 React “玩弄”的 24 小时:为了修一个不存在的 Bug,我给大模型送了顿火锅钱
前端·react.js
米丘31 分钟前
了解 Javascript 模块化,更好地掌握 Vite 、Webpack、Rollup 等打包工具
前端
Heo32 分钟前
深入 React19 Diff 算法
前端·javascript·面试
滕青山33 分钟前
个人所得税计算器 在线工具核心JS实现
前端·javascript·vue.js
小怪点点34 分钟前
手写promise
前端·promise
国思RDIF框架43 分钟前
RDIFramework.NET Web 敏捷开发框架 V6.3 发布 (.NET8+、Framework 双引擎)
前端
颜酱44 分钟前
从0到1实现LFU缓存:思路拆解+代码落地
javascript·后端·算法
Mintopia44 分钟前
如何在有限的时间里,活出几倍的人生
前端
炫饭第一名44 分钟前
速通Canvas指北🦮——变形、渐变与阴影篇
前端·javascript·程序员
Neptune11 小时前
让我带你迅速吃透React组件通信:从入门到精通(上篇)
前端·javascript