Object.keys(..)对象属性的顺序?

18,170 阅读5分钟

在开发中遇到了这样一个麻烦,需要比较两个庞大的object数据结构对应属性的值是否发生了变化,并得标记这个属性,比如:

const initialData = {name:"rockets",team:["Harden","Paul"],location:{country:"USA",city:"Houston"},goal:[{champion:true}]};

const changedData = {name:"rockets",team:["Harden","Paul","House"],location:{country:"USA",city:"Houston"},goal:[{champion:true}]};

calcEqual(initialData,changedData);
//得到类似{name:true,team:false,location:true,goal:true}结果
//true表示值一致,false表示值发生改变

首先确定的是得遍历objectkeys

遇到的问题是如何判断前后两个值是否相等,特殊的是teamlocationgoal这样引用值的比较(包括引用值还内嵌引用值)。因为内嵌之后情况比较复杂,目标指向了先字符串化,再比较:

  1. xx.toString()?像team这样没有嵌套的数组值可以比较,但是var a = {};a.toString === "[object Object]",涉及对象的比较会失败。//不能用
  2. JSON.stringify()?这里有两个问题://不能用
  3. 决定手动将引用值转化为字符串 //待定
function preTreatment(value){
  //基础类型直接返回
  if(typeof value!=="object") return String(value);
  if(Object.is(value,null)) return String(value);
  
  //引用类型
  //数组
  if(Object.prototype.toString.call(value)==="[object Array]"){
    return value.map(v=>preTreatment(v)).join(",");
  }
  
  //对象
  if(Object.prototype.toString.call(value)==="[object Object]"){
    const keys = Object.keys(value);
    
    return keys.map(k=>{
      if(typeof value[k]!=="object") return `${k}:${preTreatment(value[k])}`;
      return preTreatment(value[k]);
    }).join(",")
  }
}

这里拆解了嵌套的情况,但是存在的风险点是Object.keys()不保证属性的顺序性。从这句话开始,出发在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.

因为迭代的顺序是依赖于浏览器实现的,结论是不保证。

保证对象属性的顺序

如果Object.keys()不能保证对象属性的顺序,那么JavaScript有没有其他方法是可以保证对象属性的顺序。

ownPropertyKeys:规定对象属性遍历顺序

找到了一篇简洁风格的文章:Property order is predictable in JavaScript objects since ES2015,它提到了JavaScript内部的ownPropertyKeys方法,它定义了对象属性遍历的顺序:

  1. integer-like keys in ascending order
  2. normal keys in insertion order
  3. Symbols in insertion order
  4. if mixed, order: interger-like, normal keys, Symbols

interget-like keys,除了0,1这样的数字,也包括"2",这种键类型按照从小到大顺序返回;normal keys也就是string类型,和Symbol一样都是按照插入顺序返回。如果包含多种类型,按照 interger-like keys、normal keys、Symbols顺序从前往后。(控制台输出的顺序貌似也是这样的...)

Object.getOwnPropertyNames()

基于内部ownPropertyKeys方法实现的方法有Object.getOwnPropertyNamesReflect.ownKeys,这两种方法保证对象属性的顺序。

Reflect.ownKeys()返回的结果等价于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target)),包括直接挂在目标对象上的可枚举、不可枚举、Symbols的属性组成的数组。但是这里不选择这个方法,主要考虑兼容性(ES6提出,ie不支持):

Object.getOwnPropertyNames()返回直接挂在目标对象上的可枚举、不可枚举属性组成的,在ES5提出,兼容性更好,支持IE9+:

到这里修改下最开始贴出的preTreatment方法:

function preTreatment(value){
  //基础类型直接返回
  if(typeof value!=="object") return String(value);
  if(Object.is(value,null)) return String(value);
  
  //引用类型
  //数组
  if(Object.prototype.toString.call(value)==="[object Array]"){
    return value.map(v=>preTreatment(v)).join(",");
  }
  
  //对象
  if(Object.prototype.toString.call(value)==="[object Object]"){
    const keys = Object.getOwnPropertyNames(value);		//替换Object.keys
    
    return keys.map(k=>{
      if(typeof value[k]!=="object") return `${k}:${preTreatment(value[k])}`;
      return preTreatment(value[k]);
    }).join(",")
  }
}

手动为Object.keys()添加顺序

突然想到,虽然Object.keys()本身返回的对象属性的数组无法保证顺序,但是可以手动为返回的结果排序,再加上项目中遇到的属性名都是normal key即字符串类型,只需替换为Object.keys(value).sort()

然而 lodash...

然而问了一下同事之后,被告知lodash有一个isequal方法:

Performs a deep comparison between two values to determine if they are equivalent.

Note: This method supports comparing arrays, array buffers, booleans, date objects, error objects, maps, numbers, Object objects, regexes, sets, strings, symbols, and typed arrays. Object objects are compared by their own, not inherited, enumerable properties. Functions and DOM nodes are compared by strict equality, i.e. ===.

嗯...符合这里的场景。

然后好奇心驱使大致看了一下lodash的isequal源码,在深度比较object时,是取相同的key对应的value值作比较,也就没有字符串化的过程,也没有涉及对象属性的顺序问题...(手动这样实现有点绕,但是可以直接用,而且安全)

嗯...好吧,最后还是妥协使用了lodash的isequal方法。

小结

对象属性的遍历,在Object.keys()for..in循环时候是一个模糊地带,不保证对象属性的顺序;Object.ownPropertyNames基于内建方法ownPropertyKeys,有一个保证的对象属性顺序,而且兼容性很好,支持IE9+;

另外在日常开发中,需要方法解决难解决的问题时,优先查阅一下像lodash这样的库有没有实现的方法,提高平时开发效率,而在有空时候再去深入研究

兼容性查询工具

  • http://kangax.github.io/compat-table/es5/
  • https://caniuse.com/

参考链接

stackoverflow-Does JavaScript Guarantee Object Property Order?

Property order is predictable in JavaScript objects since ES2015