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
被调用时内部都发生了什么。
答案
对于上面那个问题先给出结论,Object.keys
在内部会根据属性名 key
的类型进行不同的排序逻辑。分三种情况:
- 如果属性名的类型是
Number
,那么Object.keys
返回值是按照key
从小到大排序。 - 如果属性名的类型是
String
,那么Object.keys
返回值是按照属性被创建的时间升序排序。 - 如果属性名的类型是
Symbol
,那么逻辑同String
相同。
这就解释了上面的问题。接下来我们详细介绍 Object.keys
被调用时,背后发生了什么。
当 Object.keys 被调用时背后发生了什么
当 Object.keys
函数使用参数 O
调用时,会执行以下步骤:
第一步:将参数转换成 Object
类型的对象。
第二步:通过转换后的对象获得属性列表 properties
。
注意:属性列表
properties
为List
类型(List
类型 是 ECMAScript 规范类型)
第三步:将 List
类型的属性列表 properties
转换为 Array
得到最终的结果。
规范中是这样定义的:
- 调用
ToObject(O)
将结果赋值给变量obj
- 调用
EnumerableOwnPropertyNames(obj, "key")
将结果赋值给变量nameList
- 调用
CreateArrayFromList(nameList)
得到最终的结果
将参数转换成 Object
ToObject
操作根据下表将参数 O
转换为 Object 类型的值:
参数类型 | 结果 |
---|---|
Undefined | 抛出 TypeError |
Null | 抛出 TypeError |
Boolean | 返回一个新的 Boolean 对象 |
Number | 返回一个新的 Number 对象 |
String | 返回一个新的 String 对象 |
Symbol | 返回一个新的 Symbol 对象 |
Object | 直接将 Object 返回 |
因为 Object.keys
内部有 ToObject
操作,所以 Object.keys
其实还可以接收其他类型的参数。上表详细描述了不同类型的参数将如何转换成 Object 类型。
我们可以简单写几个例子试一试:
先试试 null
会不会报错:
如图所示,果然报错了。接下来我们试试数字的效果:
如图所示,返回空数组。为什么会返回空数组?
上图所示,返回的对象没有任何可提取的属性,所以返回空数组也是正常的。
然后我们再试一下 String
的效果:
通过上图我们会发现返回了一些字符串类型的数字。这是因为 String
对象有可提取的属性:
因为 String 对象有可提取的属性,所以将 String
对象的属性名都提取出来变成了列表返回出去了。
获得属性列表
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
被调用时,执行以下步骤(其实就一步):
Return ! OrdinaryOwnPropertyKeys(O)
而 OrdinaryOwnPropertyKeys
是这样规定的:
- 声明变量
keys
值为一个空列表(List 类型)。 - 把每个 Number 类型的属性,按数值大小升序排序,并依次添加到
keys
中。 - 把每个 String 类型的属性,按创建时间升序排序,并依次添加到
keys
中。 - 把每个 Symbol 类型的属性,按创建时间升序排序,并依次添加到
keys
中。 - 将
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 使用,它是一个通用的规则)
将 List 类型转换为 Array 得到最终结果
对于 CreateArrayFromList(elements)
,现在我们已经得到了一个对象的属性列表,最后一步是将 List
类型的属性列表转换成 Array
类型。
将 List 类型的属性列表转换成 Array
类型非常简单:
- 先声明一个变量
array
,值是一个空数组; - 循环属性列表,将每个元素添加到
array
中; - 将
array
返回。
该顺序规则还适用于其他 API
上面介绍的排序规则同样适用于下列 API:
Object.entries
Object.values
for...in
循环Object.getOwnPropertyNames
Reflect.ownKeys
注意:以上 API 除了
Reflect.ownKeys
之外,其他 API 均会将Symbol
类型的属性过滤掉。
文章作者 Berwin,原文地址 5 分钟彻底理解 Object.keys