JavaScript 对象遍历方法及其遍历顺序的总结
最近看了《你不知道的 JavaScript(下卷)》,看到了遍历顺序相关的内容(P240),于是作出修改和补充。
2022 / 09 / 19
00. 从规范中定义的算法说起
在 ES6 之前,一个对象属性(键)的列出顺序依赖于浏览器的具体实现,并未在规范中定义。
多数引擎采用的是按照 创建顺序 进行枚举。
ES6 规范新增了一些算法的定义,来规范对象属性的列出顺序。包括:
[[OwnPropertyKeys]]
[[Enumerate]]
(于 ES2016 / ES7 废除),变更为抽象方法EnumerateObjectProperties
。
下面分别介绍这两种算法。
01. [[OwnPropertyKeys]]
1.1 列出属性的范围
这个算法会产生对象的 所有实例属性(即自身拥有的,而非继承来的)。
- 包括 字符串属性 和 Symbol 符号属性。
- 无论是否可枚举。
1.2 使用该算法的方法
这个算法只对以下方法有保证:
Reflect.ownKeys(..)
- 返回由 目标对象 自身的属性键 组成的数组。
- 相当于
Object.getOwnPropertyNames()
与Object.getOwnPropertySymbols()
得到的两个数组进行拼接。
Object.getOwnPropertyNames(..)
- 返回由 目标对象 自身的字符串属性键 组成的数组。
Object.getOwnPropertySymbols(..)
- 返回由 目标对象 自身的 Symbol 属性键 组成的数组。
Object.assign(target, ...sources)
- 将所有 可枚举的 自身的属性 从 一个或多个源对象 复制到 目标对象,返回修改后的对象。
- 其中对于每一个源对象,都使用
[[ownPropertyKeys]]
算法枚举(列出)其属性键,并过滤出可枚举的属性。
1.3 列出顺序
这个算法定义了一个对象属性的列出顺序:
- 先按照 数字上升的排序,枚举所有整数属性。
- 再按 创建顺序 枚举其余的 字符串属性 。
- 最后按 创建顺序 枚举拥有的 Symbol 符号属性。
其中,整数属性的定义为:
- +0 <=
parseFloat(key)
< 2^32 - 1 (最大安全整数) - 不作任何修改便可以与一个整数值相互转换
String(Math.trunc(Number("49"))); // "49",相同,整数属性
String(Math.trunc(Number("+49"))) ; // "49",不同于 "+49" ⇒ 不是整数属性
String(Math.trunc(Number("1.2"))) ; // "1",不同于 "1.2" ⇒ 不是整数属性
1.4 举例
let o = {
};
o[Symbol("s2")] = "foo";
o[Symbol("s1")] = "bar";
o.b = "bbbbb";
o.a = "aaaaa";
o[2] = true;
o[1] = true;
Reflect.ownKeys(o); // ['1', '2', 'b', 'a', Symbol(s2), Symbol(s1)]
Object.getOwnPropertyNames(o); // ['1', '2', 'b', 'a']
Object.getOwnPropertySymbols(o); // [Symbol(s2), Symbol(s1)]
Object.assign({
},obj);
// {1: true, 2: true, b: 'bbbbb', a: 'aaaaa', Symbol(s2): 'foo', Symbol(s1): 'bar'}
02. for..in
for..in
使用 [[enumerate]]
(ES6) / EnumerateObjectProperties
(ES7+) 来遍历对象属性。
[[enumerate]]
于 ES2016 / ES7 中废弃,更换为使用抽象方法EnumerateObjectProperties
同时,在 ES2016 中还废弃了Reflect.enumerate(..)
方法。
2.1 列出属性的范围
会产生对象 实例上和原型链上(即自身拥有的以及继承来的属性) 的 可枚举属性。
且只产生 字符串属性,忽略 Symbol 属性。
2.2 列出顺序
ES2015 - ES2019
在 ES2015 中,由于兼容性原因,[[enumerate]]
并未规定对象属性的列出顺序,可以观察到的顺序和**具体的浏览器实现相关。
即规范只限制了列出属性的范围,而没有限制列出的顺序。
在 ES2016 中,使用抽象方法 EnumerateObjectProperties
替代了 [[enumerate]]
,但同样没有规定对象属性列出的顺序。
ES2020
从 ES2020 开始,即使是较旧的操作如 for..in
、Object.keys
也需要遵循属性顺序(property order,即 [[OwnPropertyKeys]]
中定义的顺序)。
03. Object.keys()
规范定义, Object.keys()
使用 EnumerableOwnProperties 抽象方法产生对象属性列表。
- 在这个抽象方法当中,通过调用
[[OwnPropertyKeys]]
算法取得拥有的所有键的列表。 - 然后过滤掉 不可枚举(
[[Enumerable]] === false
)的属性 和 Symbol 的属性。 - 在 ES2020 前内部可能会将列表重新排列来与
for..in
的属性顺序相匹配。
与此相似,
Object.values()
Object.entries()
JSON.stringify()
(转换对象为 JSON,遍历需要使用的对象属性列表)
也是使用同样的抽象方法 EnumerableOwnProperties
来生成属性 / 值的列表。
3.1 列出属性的范围
Object.keys()
会返回由目标对象的 实例上的(自身拥有的属性,而非继承来的)可枚举属性 组成的数组。
且只产生 字符串属性,忽略 Symbol 属性。
3.2 列出顺序
属性顺序与 for..in
保持一致。
04. For…of
for…of 可以遍历可迭代对象,即实现了 Symbol.iterator
方法的对象。
4.1 列出顺序
遍历的顺序为 调用 Symbol.iterator
工厂方法返回的迭代器中, next() 方法的返回值顺序。
05. 总结
- ES6 前没有明确地规范 对象属性的列出顺序。
- ES6 定义了新的算法
[[ownPropertyKeys]]
,规定了对象属性的列出顺序。- 使用该算法的方法:
Reflect.ownKeys()
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.assign()
- 此时为了兼容性,
for..in
,Object.keys()
,JSON.stringify()
一系列方法的属性顺序没有进行规定,仍然是由浏览器自行实现。不过这些方法的属性顺序表现一致。
- 使用该算法的方法:
- ES2020 开始,
for..in
,Object.keys()
这些旧方法也需要遵循[[ownPropertyKeys]]
中定义的属性顺序。
参考文章
▲ Object.keys(…)对象属性的顺序?
▲ 一起学规范系列 —— Object.keys() 的顺序是如何定义的?
▲ 【JS】for in和for of的前世今生,从Symbol和Iterator讲起
[更新于 2022 / 09 / 19]
▲ ES6 是否为对象属性引入了明确定义的枚举顺序?
▲ EnumerateObjectProperties 规范定义