说明
图解 Google V8 学习笔记
线性结构和非线性结构
JavaScript 中的对象是由一组组属性和值的集合,就像一个字典,字符串作为键名,任意对象可以作为键值,可以通过键名读写键值。
而在 V8 实现对象存储时,并没有完全采用字典的存储方式,而是用的非线性的数据结构,查询效率会低于线性的数据结构,V8 为了提升存储和查找效率,采用了一套复杂的存储策略。下面看看是用了什么策略去提升了对象属性的访问速度?
常规属性 (properties) 和排序属性 (element)
我们先看一个例子:
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]}`) }
我们无序的给函数添加属性然后打印出来,结果如下:
我们发现:
设置的数字属性被最先并且按照数字大小的顺序打印出来;
设置的字符串属性依然是按照之前的设置顺序打印的。
总的来说就是:一个对象中如果有数字型属性和非数字型属性,当遍历对象并打印出该对象的所有属性时,会优先把数字型属性按升序排序后打印,接着再对非数字型属性按添加的先后顺序打印出来。
原因:在 ECMAScript 规范中定义了数字属性应该按照索引值大小升序排列,字符串属性根据创建时的顺序排列。
对象中的数字属性称为排序属性,在 V8 中被称为 elements
字符串属性就被称为常规属性,在 V8 中被称为 properties。
在 V8 内部,分别使用了两个线性数据结构来分别保存排序属性和常规属性,从而能有效地提升存储和访问这两种属性的性能,如图所示:
如果执行索引操作,那么 V8 会先从 elements 属性
中按照顺序读取所有的元素,然后再在 properties 属性
中读取所有的元素。
快属性和慢属性
对象内属性
将不同的属性分别保存到 elements 属性和 properties 属性中,简化了程序的复杂度,但是在查找元素时,却多了一步操作,会影响到元素的查找效率。
比如:上面执行 bar.B这个语句来查找 B 的属性值,那么在 V8 会先查找出 properties 属性所指向的对象 properties,然后再在 properties 对象中查找 B 属性
基于这个原因,V8 采取了一个权衡的策略:将部分常规属性直接存储到对象本身去加快查找属性的效率,我们把这称为对象内属性 (in-object properties)。
比如:现在要执行 bar.B这个语句来查找 B 的属性值,那么 V8 就可以直接从 bar 对象本身去获取该值了。
对象内属性是默认10,超出部分会被存放在常规属性存储中保存,可以自由扩容。
快属性
通常,我们将保存在线性数据结构中的属性称之为快属性。
因为线性数据结构中只需要通过索引即可以访问到属性,虽然访问线性结构的速度快,但是如果从线性结构中添加或者删除大量的属性时,则执行效率会非常低,这主要因为会产生大量时间和内存开销。
慢属性
如果一个对象的属性过多时,V8 就会采取另外一种存储策略,那就是慢属性策略,但慢属性的对象内部会有独立的非线性数据结构 (词典) 作为属性存储容器。所有的属性元信息不再是线性存储的,而是直接保存在属性字典中。
开启慢速模式时,使用hash作为底层存储结构,key为字符串,字面量会发生类型转换。
数组索引属性和命名属性存储在两个单独的数据结构中:
慢属性是如何存储的:
慢属性使⽤ HashMap 作为属性存储,⽽是直接存储在属性 Hash 中(没有缓存,所以叫慢属性)。
总结