沐光

记录在前端之路的点点滴滴

记 JS 对象属性顺序

前言

早期与后台对接接口时,让后台区分开数组与对象的传递参数,对于为什么也仅仅只是知道对象遍历顺序无法像数组一样得到保证,具体是怎么个规则还是不太了解。最近在学习了解 Reflect 部分知识时,正好又看到了对象属性排序部分的内容,因此做个笔记记录下来,用作备忘。

该部分基本上来自书本内容,夹带一些个人理解

ES6 属性顺序

对于 ES6 之前,一个对象的属性列出的顺序是依赖于具体实现,并未在规范中定义。虽然多数引擎按照创建的顺序进行枚举,但是开发者们一直强烈建议不要依赖这个顺序。

而对于 ES6 来说,拥有属性的列出顺序则是又 [[OwnPropertyKeys]] 算法定义的(ES6 规范 9.1.12 节),其顺序为:

  1. 首先,按照数字上升排序,枚举所有的整数索引拥有的属性;
  2. 然后,按照创建顺序枚举其余的拥有的字符串属性名;
  3. 最后,按照创建顺序枚举拥有的符号属性。

此顺序只对 Reflect.ownKeys(...) (以及扩展的 Object.getWonPropertyNames(...)Object.getOwnPropertySymbols(...) )有保证,为 [[OwnPropertyKeys]] 算法。

此算法不会遍历原型链上的属性

1
2
3
4
5
6
7
8
9
const obj = {};

obj[Symbol('a')] = 'symbol property';
obj[1] = 'number property';
obj.str = 'string property';

Reflect.ownKeys(obj); // ["1", "str", Symbol(a)]
Object.getOwnPropertyNames(obj); // ["1", "str"]
Object.getOwnPropertySymbols(obj); // [Symbol(a)]

枚举算法

ES6 规范中还有另外一种 [[Enumerate]] 算法(ES6 规范 9.1.11 节),其只从目标对象和 prototype 原型链产生可枚举属性。其可以观察到的顺序与具体实现有关,不由规范控制。

对于日常工作中常用的 Object.keys(...)for ... inJSON.stringify(...)(还有 Reflect.enumerate(...))基本上属于 [[Enumerate]] 算法。此四种方法虽然严格上是通过不同路径实现排序的,但将其与 [[OwnPropertyKeys]] 的排序相匹配的具体实现还是允许的,但些许又些差别:

附:本质上 Reflect.enumerate(...)for ... in 的实现方式是一样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Parent() {}
Parent.prototype = {
age: 123,
2: 234,
[Symbol('b')]: 345
}

const obj = new Parent();

obj[Symbol('a')] = 'symbol property';
obj[1] = 'number property';
obj.str = 'string property';

console.log(Object.keys(obj)); // ['1', 'str']
console.log(JSON.stringify(obj)); // {"1":"number property","str":"string property"}

// 对象 -> 原型链,每一层的显示方式同上面的规范,整体上的排序则是和具体实现相关
// 此处的具体实现则是先对象这一层,然后原型链层
for (const prop in obj) {
console.log(prop); // 1 str 2 age
}

总结

对于 ES6 来说,Reflect.ownKeys(...)Object.getWonPropertyNames(...)Object.getOwnPropertySymbols(...) 的顺序都是可预测且可靠的,这由规范保证。所以依赖这个顺序的代码是安全的。

Reflect.enumerate(...)、Object.keys(…)for … in(以及扩展的JSON.stringify(…))还像过去一样,可观察到顺序是相同的。但是这个顺序不再必须与Reflect.ownKeys(…)` 相同。在使用它们依赖于具体实现的顺序时要小心

扩展阅读