JavaScript的对象是一组组属性和值的集合,这很像一个字典,字符串作为键名,任意对象可以作为键值。
然而出于性能的考量,V8 实现对象存储时,并没有完全采用字典的存储方式。因为字典是非线性的数据结构,查询效率会低于线性的数据结构,V8 为了提升存储和查找效率,采用了一套复杂的存储策略。
常规属性和排序属性
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]}`)
}
结果:
index:1 value:test-1
index:3 value:test-3
index:5 value:test-5
index:8 value:test-8
index:9 value:test-9
index:50 value:test-50
index:100 value:test-100
index:B value:bar-B
index:A value:bar-A
index:C value:bar-C
可以发现:数字属性最先打印,并且按照数字大小的顺序打印;字符串属性是按设置顺序打印的。
ECMAScript 规范定义:数字属性按照索引值大小升序排列,字符串属性根据创建时的顺序升序排列。
我们把对象中数字属性称为排序属性,在 V8 中被称为 elements,字符串属性就被称为常规属性,在 V8 中被称为 properties。在 V8 内部,为了有效地提升存储和访问这两种属性的性能,分别使用了两个线性数据结构来分别保存排序属性和常规属性,具体结构如下图所示:
分解成这两种线性数据结构之后,如果执行索引操作,那么 V8 会先从 elements 属性中按照顺序读取所有的元素,然后再在 properties 属性中读取所有的元素,这样就完成一次索引操作。
快属性和慢属性
将不同的属性分别保存到 elements 属性和 properties 属性中,简化了程序的复杂度,但是在查找元素时,却多了一步操作,比如执行 bar.B这个语句来查找 B 的属性值,那么在 V8 会先查找出 properties 属性所指向的对象 properties,然后再在 properties 对象中查找 B 属性,这种方式在查找过程中增加了一步操作,因此会影响到元素的查找效率。
基于这个原因,V8 采取了一个权衡的策略以加快查找属性的效率,这个策略是将部分常规属性直接存储到对象本身,这些就被称为对象内属性 (in-object properties)。对象在内存中的展现形式你可以参看下图:
这种方式减少查找属性值的步骤,增加了查找效率。不过对象内属性的数量是固定的,默认是 10 个,如果添加的属性超出了对象分配的空间,则它们将被保存在常规属性存储中。
快属性:保存在线性数据结构中的属性。通过索引即可以访问到属性,速度快,但添加或者删除大量的属性时,会产生大量时间和内存开销,执行效率会非常低。
慢属性:如果一个对象的属性过多时,V8 采取的另外一种存储策略。慢属性的对象内部会有独立的非线性数据结构 (词典) 作为属性存储容器。所有的属性元信息不再是线性存储的,而是直接保存在属性字典中,如图:
总结:
- 对象的数字属性存储在线性结构中,按索引升序排列;
- 对象的非数字属性按创建时的顺序排列(一般情况下)。如果属性数量少于10个,直接存储在对象内(对象内属性);如果大于10而小于20个,则多出来的存储在properties线性结构对象中;如果数量大于20个,则多出来的属性存储在properties非线性结构对象中。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。