分析JS对象内部属性的遍历顺序

背景

在我们日常的开发中,有很多下拉选框里面的选项值是通过后端传回来的枚举值(enums)来作为渲染的数据的。例如这样子的下拉选框:
如果我们需要让下拉选框里面的选项顺序,是依据后端返回的数据进行排序,那这个时候就有两种方法。一种是后端返回一个数组数据,然后我们按照顺序遍历这个数据数据。但如果是后端返回的是这样的一个对象,
那我们就需要考虑一下Object.keys(), for ... in 等一系列的遍历对象的方法能否按照后端属性创建的顺序来输出了。

问题重现

我们先看一下下面这段代码:

var obj = {
      name: 'abc',
      3: 'ccc',
      age: 23,
      class: 'first',
      hobby: 'basketball'
};
console.log(Object.keys(obj));
// ["3", "name", "age", "class", "hobby"]

可以看出,Object.keys()是无法保证输出的属性的顺序的。

Object.keys不保证对象属性的顺序?

MDN-Object.keys中描述是这样的

Object.keys() returns an array whose elements are strings corresponding to the enumerable properties found directly upon object. The ordering of the properties is the same as that given by looping over the properties of the object manually.

说是和手动遍历相同,那再看一下MDN-for..in

Array indexes are just enumerable properties with integer names and are otherwise identical to general object properties. There is no guarantee that for...in will return the indexes in any particular order...Because the order of iteration is implementation-dependent.

最后一句表达了不同浏览器的实现方式是不同的。 但在一篇文章中描述Property order is predictable in JavaScript objects since ES2015
根据文章重的描述是 Object.getOwnPropertyNames, Reflect.ownKeys他们都是内部通过ownPropertyKeys这个方法实现的,而且在文章中他还给出了对象中属性的输出顺序的规则

  1. 数字或者字符串类型的数字当作key时,输出是按照升序排序的
  2. 普通的字符串类型的key,就按照定义的顺序输出
  3. Symbols也是和字符串类型的规则一样
  4. 如果是三种类型的key都有,那么顺序是 1 -> 2 -> 3

Reflect.ownKeys()在IE中是不支持的

Object.getOwnPropertyNames()的兼容性更好

但如果是想用Object.keys,这个是否是基于ownPropertyKeys方法,还要根据浏览器实现而不同。

标准参考

根据 ECMA-262(ECMAScript)第三版中描述,for-in 语句的属性遍历的顺序是由对象定义时属性的书写顺序决定的。

在现有最新的 ECMA-262(ECMAScript)第五版规范中,对 for-in 语句的遍历机制又做了调整,属性遍历的顺序是没有被规定的。

经过上面问题重现的代码复现以及查阅网上的信息,Chrome Opera 的 JavaScript 解析引擎遵循的是新版 ECMA-262 第五版规范。因此,使用 for-in 语句遍历对象属性时遍历顺序并非属性构建顺序。而 IE6 IE7 IE8 Firefox Safari 的 JavaScript 解析引擎遵循的是较老的 ECMA-262 第三版规范,属性遍历顺序由属性构建的顺序决定。

Chrome的Object.keys的输出顺序以及原因分析

Chrome 的 JS 引擎遍历对象属性时会遵循一个规律:

它们会先提取所有 key 的 parseFloat 值为非负整数的属性,然后根据数字顺序对属性排序首先遍历出来,然后按照对象定义的顺序遍历余下的所有属性。例如下面的输出:

let obj = {
    'b': 'testb',
    'a': 'testa',
    '1': 'test1',
    '测': 'test测',
    '2': 'test2'
}

console.log(Object.keys(obj));

// [1, 2, 'b', 'a', '测']

Chrome有这样的表现是因为V8引擎为了提高对象的访问速度,V8 里的对象就维护两个属性,会把数字放入线性的 elements 属性中,并按照顺序存放。会把非数字的属性放入 properties 中,不会排序。寻找属性时先 elements 而后在 properties。V8引擎这样做的原因可以在李兵老师的V8采用了哪些策略提升了对象属性的访问速度?中找到。

总结

我们目前的项目是只需要兼容Chrome浏览器即可,可以看出只要是数组的返回值里面,属性值的key不是数字与字符串混合用,那么完全可以使用Object.keys并且可以保证读取时的顺序的,保证下拉选项渲染的顺序,无需后端为此改为数组返回,后端也省去了把之前数据和代码全部更改的工作量和风险。

引用

为什么 JS 对象内部属性遍历的顺序乱了
Object.keys(..)对象属性的顺序?
Property order is predictable in JavaScript objects since ES2015
js中关于for...in遍历对象属性的顺序问题