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 内部,为了有效地提升存储和访问这两种属性的性能,分别使用了两个线性数据结构来分别保存排序属性和常规属性,具体结构如下图所示:
image.png
分解成这两种线性数据结构之后,如果执行索引操作,那么 V8 会先从 elements 属性中按照顺序读取所有的元素,然后再在 properties 属性中读取所有的元素,这样就完成一次索引操作。

快属性和慢属性
将不同的属性分别保存到 elements 属性和 properties 属性中,简化了程序的复杂度,但是在查找元素时,却多了一步操作,比如执行 bar.B这个语句来查找 B 的属性值,那么在 V8 会先查找出 properties 属性所指向的对象 properties,然后再在 properties 对象中查找 B 属性,这种方式在查找过程中增加了一步操作,因此会影响到元素的查找效率。
基于这个原因,V8 采取了一个权衡的策略以加快查找属性的效率,这个策略是将部分常规属性直接存储到对象本身,这些就被称为对象内属性 (in-object properties)。对象在内存中的展现形式你可以参看下图:
image.png
这种方式减少查找属性值的步骤,增加了查找效率。不过对象内属性的数量是固定的,默认是 10 个,如果添加的属性超出了对象分配的空间,则它们将被保存在常规属性存储中。

快属性:保存在线性数据结构中的属性。通过索引即可以访问到属性,速度快,但添加或者删除大量的属性时,会产生大量时间和内存开销,执行效率会非常低。
慢属性:如果一个对象的属性过多时,V8 采取的另外一种存储策略。慢属性的对象内部会有独立的非线性数据结构 (词典) 作为属性存储容器。所有的属性元信息不再是线性存储的,而是直接保存在属性字典中,如图:

image.png

总结:

  • 对象的数字属性存储在线性结构中,按索引升序排列;
  • 对象的非数字属性按创建时的顺序排列(一般情况下)。如果属性数量少于10个,直接存储在对象内(对象内属性);如果大于10而小于20个,则多出来的存储在properties线性结构对象中;如果数量大于20个,则多出来的属性存储在properties非线性结构对象中。

淡淡风云
0 声望0 粉丝