for...in和Object.keys迭代顺序的深入分析

先说结论

for...inObject.keys对可枚举属性进行遍历的顺序与属性的书写顺序不一定一致,因为在内部会根据属性key值的类型进行不同的排序逻辑,分三种情况:

  1. 如果属性名的类型是Number,那么Object.keys返回值是按照key从小到大排序
  2. 如果属性名的类型是String,那么Object.keys返回值是按照属性被创建的时间升序排序。

背后的执行机制

对象的属性可分为常规属性排序属性两种,在ECMAScript规范中定义了 「数字属性应该按照索引值大小升序排列,字符串属性根据创建时的顺序升序排列。」在这⾥我们把对象中的数字属性称为「排序属性」,在V8中被称为elements,字符串属性就被称为「常规属性」, 在V8中被称为properties。在V8内部,为了有效地提升存储和访问这两种属性的性能,分别使⽤了两个线性数据结构来分别保存排序属性和常规属性。

以下面这段代码为例进行说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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

在V8内部的存储结构如下图所示:

在elements对象中,会按照顺序存放排序属性,properties属性则指向了properties对 象,在properties对象中,会按照创建时的顺序保存了常规属性。