深入使用 Object.keys()

2,150 阅读4分钟

「这是我参与2022首次更文挑战的第9天,活动详情查看:2022首次更文挑战」。

Object.keys() 是干嘛的

  • 其实不用过多介绍,大家或多或少都使用过 Object.keys()
  • 它接受一个对象,返回其可枚举的属性名的列表,包括该对象继承的属性
  • MDN 上有说,它输出的属性名顺序与正常使用 for/in 循环遍历的顺序相同,但是没有说是按什么顺序排列的
  • 既然输出的是一个属性名的列表,那列表会不会涉及到某种排序规则呢?
  • 闲来无事,自己探究一波

Object.keys() 返回的数组中,属性名是如何排序的?

  • 先写个代码跑一下
class A {
  constructor() {
    this.a = "";
  }
}

class B extends A {
  constructor() {
    super();
    this.b = "";
  }
}

const obj = new B();

console.log(Object.keys(obj));
  • 毫无疑问,上面打印的数组为[ 'a', 'b' ]
  • 那为什么是 [ 'a', 'b' ],而不是 [ 'b', 'a' ] 呢?
  • 我们修改一下代码,然后输出看看
class A {
  constructor() {
    this.b = "";
  }
}

class B extends A {
  constructor() {
    super();
    this.a = "";
  }
}

const obj = new B();

console.log(Object.keys(obj));
  • 执行后发现,现在输出的是 [ 'b', 'a' ],输出结果变了,但是输出的属性名顺序没变

  • 依然是先输出的类 A 上的属性,然后输出类 B 上的属性

  • 我们都知道,es6 中类的继承过程是这样的

    • 会先创建实例,然后将父类的属性和方法加到实例上
    • 然后才是将子类的属性和方法加到实例上
  • 由于对象先添加的属性 b,再添加的属性 a, 所以输出 [ 'b', 'a' ] 很合理

  • 那我们可以认为 Object.keys() 输出的列表是按照原对象属性添加的先后顺序来输出的吗?

  • 再来个栗子 🌰,大家看看输出的是啥

const obj = {};

obj["d"] = "";
obj["a"] = "";
obj["b"] = "";
obj["c"] = "";

console.log(Object.keys(obj));
  • 上面输出的是 [ 'd', 'a', 'b', 'c' ]

  • 符合上面的猜测,输出很合理

  • 再来个栗子 🌰,大家看看输出的是啥

const obj = {};

obj[4] = "";
obj[1] = "";
obj[3] = "";
obj[2] = "";

console.log(Object.keys(obj));
  • 上面输出的是 [ '1', '2', '3', '4' ]
  • 神奇的事情发生了,为啥不是 [ '4', '1', '3', '2' ] 呢?
  • 如果我们将上面两个例子合并一下输出的结果又是什么呢?
const obj = {};

obj["d"] = "";
obj["a"] = "";
obj["b"] = "";
obj["c"] = "";
obj[4] = "";
obj[1] = "";
obj[3] = "";
obj[2] = "";

console.log(Object.keys(obj));
  • 上面输出的是 ['1', '2', '3', '4', 'd', 'a', 'b', 'c' ]

  • 这个结果与上面的例子似乎有着某种相似性,但是有说不出个所以然

  • 这个时候直觉告诉我,需要开始找资料了

  • 于是我开始一番搜索后发现,MDN 上有写,Object.keys() 的 Polyfill 实现中,就是用 for/in 实现的,所以 for/in 输出结果的顺序就是 Object.keys() 输出的顺序

  • 根据 MDN 提供的关于 Object.keys() 的 ECMAScript 规范链接,一层一层找下去,能够看到如下规则: image.png

  • 翻译过来大概意思就是:

    • 第一步,会创建一个空列表 keys,用于存放属性名
    • 第二步,将属性名是合法数组索引值的,按照升序依次存入列表
    • 第三步,将属性名是字符串类型的,按照创建的时间先后顺序依次存入列表
    • 第四步,将属性名是 Symbol 类型的,也按照创建的时间先后顺序一次存入列表
    • 第五步,返回列表 keys
  • 这么看来,上面的输出结果就非常合理了

  • 那接下来我们最后看一个例子会输出什么

const obj = {};

obj["d"] = "";
obj["a"] = "";
obj["b"] = "";
obj["c"] = "";
obj[4] = "";
obj[1] = "";
obj[3] = "";
obj[2] = "";
obj[0.9] = "";
obj[Symbol("a")] = "";
obj[Symbol("b")] = "";
obj[Symbol(2)] = "";
obj[Symbol(1)] = "";

console.log(Object.keys(obj));
  • 上面输出的是 ['1', '2', '3', '4', 'd', 'a', 'b', 'c', '0.9' ]
  • 神奇的事情又发生了
    1. 0.9 不也是数字吗,为啥在最后面
    2. Symbol 类型的属性名呢,咋没有了
  • 我们看下上面规则的第二步,说的是合法数组索引值,其实说的就是正整数,0.9 不是正整数,所以被当作字符类型处理了
  • 那 Symbol 呢?
  • 上面我们说到了,Object.keys() 的实现中使用了 for/in
  • MDN 上有说到,for/in 输出的结果是不包括 Symbol 类型的,要输出 Symbol 类型属性需要用到 getOwnPropertySymbols 方法
  • 这样看来,上面的例子也就都合理了😄

最后

  • Object.keys() 的分享就到这里,怎么样它输出的结果的顺序规则,你 xuefei 了吗👀

  • 如果觉得文章写的不错的话,希望大家不要吝惜点赞,大家的鼓励是我分享的最大动力 🥰