javascript对象的v8底层原理实现

1,705 阅读3分钟

javascript对象概念

javascript 对象像⼀个字典是由⼀组组属性和值组成的,所以最简单的⽅式是使⽤⼀个字典来保存属性和值,但是由于字典是⾮线性结构,所以如果使⽤字典,读取效率会⼤⼤降低。

V8 为了提升存储和查找效率,V8 在对象中添加了两个隐藏属性,排序属性(element)和常规属性(properties),element 属性指向了elements 对象,在 elements 对象中,会按照顺序存放排序属性。properties 属性则指向了 properties 对象,在properties 对象中,会按照创建时的顺序保存常规属性。

排序属性(elements)和常规属性(properties)

这样子讲很多人还不是很理解,下面我们写一个小小的案例来说清楚

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
         window.aobj = {
            0: 'str0',
            3: 'str1',
            2: 'str2',
            'a': 'aa',
            'b': 'bb',
            'c': 'cc',
            'd': 'dd',
            'e': 'ee',
        }
				//  动态添加属性
         for(let i=0; i<4;i++){
            aobj[`property${i}`] = Math.random()
        } 

        console.log(aobj)
    </script>
</body>

</html>

接下来我们打开浏览器开发者工具,打开Memory选项,然后录制,通过内存快照我们可以收集内存管理的信息,然后打开window/file对象,这里可以找到aobj变量的内存是怎么样的,如下图

通过上面的图片我们可以看到aobj对象里面不仅有我们定义的key,同时还维护了elements和properties这两个对象,在对象中的数字属性称为排序属性,在 V8 中被称为 elements(elements 对象中,会按照顺序存放排序属性),字符串属性就被称为常规属性,在 V8 中被称为 properties(按照创建时的顺序保存了常规属性)。aobj 对象恰好包含了这两个隐藏属性。

在 V8 内部,为了有效地提升存储和访问这两种属性的性能,分别使⽤了两个线性数据结构来分别保存排序属性和常规属性。分解成这两种线性数据结构之后,如果执⾏索引操作,那么 V8 会先从elements 属性中按照顺序读取所有的元素,然后再在 properties 属性中读取所有的元素,这样就完成⼀次索引操作。

我们来验证打印⼀下

如果在遍历对象属性的时候

  1. 数字属性被最先打印出来了,并且是按照数字⼤⼩的顺序打印的
  2. 设置的字符串属性依然是按照之前的设置顺序打印的

原因:ECMAScript 规范中定义了数字属性应该按照索引值⼤⼩升序排列,字符串属性根据创建时的顺序升序排列

下面我们再举个例子

<script>
        function Foo() {
            this[100] = 'test-100'
            this[1] = 'test-1'
            this["B"] = 'bar-B'
            this[50] = 'test-50'
            this[9] = 'test-9'
            this[8] = 'test-8'
            this[3] = 'test-3'
            this[5] = 'test-5'
            this["A"] = 'bar-A'
            this["C"] = 'bar-C'
        }
        var bar = new Foo()
        for (key in bar) {
            console.log(`index:${key} value:${bar[key]}`)
        }
        console.log(bar);
    </script>

bar对象的内存结构如下图

当我们在浏览器⾥内存快照中,并没有发现 properties,原因是bar.B这个语句来查找 B 的属性值,那么在 V8会先查找出 properties 属性所指向的对象 properties,然后再在 properties 对象中查找 B 属性,这种⽅式在查找过程中增加了⼀步操作,因此会影响到元素的查找效率。所以V8 采取了⼀个权衡的策略以加快查找属性的效率,这个策略是将部分常规属性直接存储到对象本身,我们把 这称为对象内属性 (in-object properties)。对象在内存中的展现形式你可以参看下图:

不过对象内属性的数量是固定的,默认是 10 个,如果添加的属性超出了对象分配的空间,则它们将被保存在常规属性存储中。虽然属性存储多了⼀层间接层,但可以⾃由地扩容。

<script>
        //我们⼀起测试⼀下V8
        function Foo(property_num, element_num) {
            //添加可索引属性
            for (let i = 0; i < element_num; i++) {
                this[i] = `element${i}`
            }
            //添加常规属性
            for (let i = 0; i < property_num; i++) {
                let ppt = `property${i}`
                this[ppt] = ppt;
            }
        }
        var bar = new Foo(10, 10)
    </script>

这个例子的常规属性是10个,所以此时是没有properties对象的,如下图:

这个是时候是没有properties对象的

当我们把常规属性的数量设置为20的时候我们来看看bar此时的内存快照是怎么样的,如下图

 <script>
        //我们⼀起测试⼀下V8
        function Foo(property_num, element_num) {
            //添加可索引属性
            for (let i = 0; i < element_num; i++) {
                this[i] = `element${i}`
            }
            //添加常规属性
            for (let i = 0; i < property_num; i++) {
                let ppt = `property${i}`
                this[ppt] = ppt;
            }
        }
        var bar = new Foo(20, 10)
    </script>

这里会把超出来的常规属性放在properties对象中去,类似下面的图的结构

保存在线性数据结构中的属性称之为“快属性”,因为线性数据结构中只需要通过索引即可以访问到属性,虽然访问线性结构的速度快,但是如果从线性结构中添加或者删除⼤量的属性时,则执⾏效率会⾮常低,这主要因为会产⽣⼤量时间和内存开销。因此,如果⼀个对象的属性过多时,V8 就会采取另外⼀种存储策略,那就是“慢属性”策略,但慢属性的对象内部会有独⽴的⾮线性数据结构 (词典) 作为属性存储容器。所有的属性元信息不再是线性存储的,⽽是直接保存在属性字典中。

通过引⼊这两个属性,加速了 V8 查找属性的速度,为了更加进⼀步提升查找效率,V8 还实现了内置属性的策略,当常规属性少于⼀定数量时,V8 就会将这些常规属性直接写进对象中,这样⼜节省了⼀个中间步骤。

最后如果对象中的属性过多时,或者存在反复添加或者删除属性的操作,那么 V8 就会将线性的存储模式降级为⾮线性的字典存储模式,这样虽然降低了查找速度,但是却提升了修改对象的属性的速度。

总结

javascript对象在V8底层 为了提升存储和查找效率,V8 在对象中添加了两个隐藏属性,排序属性(element)和常规属性(properties)

对象的key是数字的时候会添加到element对象中去,当初始化的常规属性超过10个的时候会把超过的数据添加到properties对象中去维护或者是动态的添加属性也会放在properties中去。

当访问属性的时候,先去element或者properties两个线性结构里面查找,查找不到再去常规属性查找,这样子是为了加快查找效率

参考

v8.dev/blog/fast-p…