Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

5分钟彻底理解Object.keys #24

Open
berwin opened this issue Jul 25, 2018 · 1 comment
Open

5分钟彻底理解Object.keys #24

berwin opened this issue Jul 25, 2018 · 1 comment
Assignees

Comments

@berwin
Copy link
Owner

berwin commented Jul 25, 2018

5分钟彻底理解Object.keys

封面图

前几天一个朋友问了我一个问题:为什么Object.keys的返回值会自动排序?

例子是这样的:

const obj = {
  100: '一百',
  2: '二',
  7: '七'
}
Object.keys(obj) // ["2", "7", "100"]

而下面这例子又不自动排序了?

const obj = {
  c: 'c',
  a: 'a',
  b: 'b'
}
Object.keys(obj) // ["c", "a", "b"]

当朋友问我这个问题时,一时间我也回答不出个所以然。故此去查了查ECMA262规范,再加上后来看了看这方面的文章,明白了为什么会发生这么诡异的事情。

故此写下这篇文章详细介绍,当Object.keys被调用时内部都发生了什么。

1. 答案

对于上面那个问题先给出结论,Object.keys在内部会根据属性名key的类型进行不同的排序逻辑。分三种情况:

  1. 如果属性名的类型是Number,那么Object.keys返回值是按照key从小到大排序
  2. 如果属性名的类型是String,那么Object.keys返回值是按照属性被创建的时间升序排序。
  3. 如果属性名的类型是Symbol,那么逻辑同String相同

这就解释了上面的问题。

下面我们详细介绍Object.keys被调用时,背后发生了什么。

2. 当Object.keys被调用时背后发生了什么

Object.keys函数使用参数O调用时,会执行以下步骤:

第一步:将参数转换成Object类型的对象。

第二步:通过转换后的对象获得属性列表properties

注意:属性列表properties为List类型(List类型ECMAScript规范类型

第三步:将List类型的属性列表properties转换为Array得到最终的结果。

规范中是这样定义的:

  1. 调用ToObject(O)将结果赋值给变量obj
  2. 调用EnumerableOwnPropertyNames(obj, "key")将结果赋值给变量nameList
  3. 调用CreateArrayFromList(nameList)得到最终的结果

2.1 将参数转换成Object(ToObject(O)

ToObject操作根据下表将参数O转换为Object类型的值:

参数类型 结果
Undefined 抛出TypeError
Null 抛出TypeError
Boolean 返回一个新的 Boolean 对象
Number 返回一个新的 Number 对象
String 返回一个新的 String 对象
Symbol 返回一个新的 Symbol 对象
Object 直接将Object返回

因为Object.keys内部有ToObject操作,所以Object.keys其实还可以接收其他类型的参数。

上表详细描述了不同类型的参数将如何转换成Object类型。

我们可以简单写几个例子试一试:

先试试null会不会报错:

Object.keys(null)

图1 Object.keys(null)

如图1所示,果然报错了。

接下来我们试试数字的效果:

Object.keys(123)

图2 Object.keys(123)

如图2所示,返回空数组。

为什么会返回空数组?请看图3:

new Number(123)

图3 new Number(123)

如图3所示,返回的对象没有任何可提取的属性,所以返回空数组也是正常的。

然后我们再试一下String的效果:

Object.keys('我是Berwin')

图4 Object.keys('我是Berwin')

图4我们会发现返回了一些字符串类型的数字,这是因为String对象有可提取的属性,看如图5:

new String('我是Berwin')

图5 new String('我是Berwin')

因为String对象有可提取的属性,所以将String对象的属性名都提取出来变成了列表返回出去了。

2.2 获得属性列表(EnumerableOwnPropertyNames(obj, "key")

获取属性列表的过程有很多细节,其中比较重要的是调用对象的内部方法OwnPropertyKeys获得对象的ownKeys

注意:这时的ownKeys类型是List类型,只用于内部实现

然后声明变量properties,类型也是List类型,并循环ownKeys将每个元素添加到properties列表中。

最终将properties返回。

您可能会感觉到奇怪,ownKeys已经是结果了为什么还要循环一遍将列表中的元素放到properties中。

这是因为EnumerableOwnPropertyNames操作不只是给Object.keys这一个API用,它内部还有一些其他操作,只是Object.keys这个API没有使用到,所以看起来这一步很多余。

所以针对Object.keys这个API来说,获取属性列表中最重要的是调用了内部方法OwnPropertyKeys得到ownKeys

其实也正是内部方法OwnPropertyKeys决定了属性的顺序。

关于OwnPropertyKeys方法ECMA-262中是这样描述的:

O的内部方法OwnPropertyKeys被调用时,执行以下步骤(其实就一步):

  1. Return ! OrdinaryOwnPropertyKeys(O).

OrdinaryOwnPropertyKeys是这样规定的:

  1. 声明变量keys值为一个空列表(List类型)
  2. 把每个Number类型的属性,按数值大小升序排序,并依次添加到keys
  3. 把每个String类型的属性,按创建时间升序排序,并依次添加到keys
  4. 把每个Symbol类型的属性,按创建时间升序排序,并依次添加到keys
  5. keys返回(return keys

上面这个规则不光规定了不同类型的返回顺序,还规定了如果对象的属性类型是数字,字符与Symbol混合的,那么返回顺序永远是数字在前,然后是字符串,最后是Symbol。

举个例子:

Object.keys({
  5: '5',
  a: 'a',
  1: '1',
  c: 'c',
  3: '3',
  b: 'b'
})
// ["1", "3", "5", "a", "c", "b"]

属性的顺序规则中虽然规定了Symbol的顺序,但其实Object.keys最终会将Symbol类型的属性过滤出去。(原因是顺序规则不只是给Object.keys一个API使用,它是一个通用的规则)

2.3 将List类型转换为Array得到最终结果(CreateArrayFromList( elements )

现在我们已经得到了一个对象的属性列表,最后一步是将List类型的属性列表转换成Array类型。

将List类型的属性列表转换成Array类型非常简单:

  1. 先声明一个变量array,值是一个空数组
  2. 循环属性列表,将每个元素添加到array
  3. array返回

3. 该顺序规则还适用于其他API

上面介绍的排序规则同样适用于下列API:

  1. Object.entries
  2. Object.values
  3. for...in循环
  4. Object.getOwnPropertyNames
  5. Reflect.ownKeys

注意:以上API除了Reflect.ownKeys之外,其他API均会将Symbol类型的属性过滤掉。

@cookie-zhang
Copy link

今天就遇到这个问题了 很不错

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants