一起学JS规范系列 —— Object.keys() 的顺序是如何定义的?

2117次阅读  |  发布于3年以前

一个有意思的问题

Object.keys() 是一个我们在写代码时非常常用的获取对象属性键值对中的键列表的方法,于此同时我们会发现它返回的键数组顺序和我们申明的属性不一致:

// 执行环境 node-v14.16.1
const object = { a: 'x', c: 'x', 55: 'x', 1: 'x', b: 'x' };

// 输出 [ '1', '55', 'a', 'c', 'b' ]
console.log(Object.keys(object));

看起来引擎似乎默认做了一些处理:

本文将基于最新的 ECMAScript® 2022 Language Specification 来看看 Object.keys() 返回的属性值顺序究竟是如何定义的。

如何下手?

我相信很多同学遇到这类问题第一反应都是去找规范定义,但是又不知道如何下手,借着这个问题正好也给同学们演示下如何从规范中找到我们想要的信息。

最新的 ECMAScript 规范可以通过访问 获得:

https://tc39.es/ecma262/

首页左上角会有一个搜索框,我们可以对想要了解的语法规范进行搜索:

在这个例子中,我们可以直接输入 Object.keys() 来获取规范的详细定义:

解读 Object.keys(O)

规范中对 Object.keys(O) 的定义写的比较简单,一共就三行,我们逐行进行解读

1. Let obj be ? ToObject(O).

Let obj be ? ToObject(O).

这句话的意思是定义一个变量 obj,其值为 ToObject(O),其中的 OObject.keys 方法中传入的参数:

When thekeysfunction is called with argument O, the following steps are taken:

规范中对 ToObject 也做了详细的定义:

显然,如果我们给 Object.keys 传入 undefined 或者 null,按照规范会抛出一个 TypeError, 事实究竟是否如此呢?

我们可以简单进行测试:

结果确实是引擎完全参照规范的定义进行了实现。

回到我们最初的例子,传入的参数是一个对象,符合这里的最后一条,因此直接返回入参,即变量 obj 的值就是传入的对象本身。

2 . Let nameList be ? EnumerableOwnPropertyNames(obj, key).

Let nameList be ? EnumerableOwnPropertyNames(obj, key).

这句话的意思是继续定一个变量 nameList,其值为 EnumerableOwnPropertyNames(obj, key) 得到的结果。

注意这里的第一个参数 obj 即为我们在第一步中定义的变量,而第二个参数 key 是一个字面量,其值就是 key

继续查看规范对 EnumerableOwnPropertyNames 的定义:

这里的步骤抽象逻辑如下所示:

前三步比较容易理解,需要注意的是规范中的 [[]] 一般指的是不能直接访问的内部属性或者内部方法,[[OwnPropertyKeys]] 具体定义本文后续会详细展开,目前只需要了解其返回了参数 O 的属性列表。

第四步稍微复杂一些,其实就是遍历第二步中得到的 ownKeys 列表,且对于其中的每一个值,获取入参 O 对此值的属性描述对象:

那么就将符合条件的 ownKeys 中的值添加到上面定义的 properties 列表中,最后返回最终得到的 properties 列表。

综上,这一步完成了 nameList 的赋值,其实就是入参对象 O 的可数属性列表。

3. Return CreateArrayFromList(nameList).

Return CreateArrayFromList(nameList).

这一句的意思就比较简单了,将上一步获取的 nameList 转化为一个数组。

可是到目前为止我们似乎还没见到 Object.keys()在哪里对属性列表进行了排序,很多同学会有疑问是不是将属性列表转化为数组的时候进行了排序呢?

我们继续查看 CreateArrayFromList 的规范定义:

这个定义相对来说简单一些,仅仅是将入参列表转换为符合 ECMAScript 规范定义的数组对象,并没有对原始列表中的值进行特别的排序。

那么到底是什么地方做了特别的排序处理呢?

让我们把目光回到第二步中讲解的 EnumerableOwnPropertyNames 时忽略的 O.[[OwnPropertyKeys]]() 内部函数。

解读 O.[[OwnPropertyKeys]]()

规范中对 Object 的各个内部方法和插槽的定义位于:Ordinary Object Internal Methods and Internal Slots,这里我们重点关注 [[OwnPropertyKeys]] ( ) 方法:

继续查看 OrdinaryOwnPropertyKeys 的定义:

继续按照定义步骤拆解:

其实到此谜底已经揭晓,正是在 [[OwnPropertyKeys]] 中对原始的对象属性进行了分类和排序,因此第一节的示例中不管属性 155 在对象中的定义位置,都会最先升序输出,其次才是属性 a , cb 按照定义的时间先后顺序输出。

array index?

思考下面的这个例子:

// 执行环境 node-v14.16.1
const object = { a: 'x', c: 'x', 55: 'x', 1: 'x', b: 'x' };
object['-1'] = 'x';
object[Math.pow(2, 32) - 1] = 'x';
object[Math.pow(2, 32) - 2] = 'x';

// 输出 [ '1', '55', '4294967294', 'a', 'c', 'b', '-1', '4294967295' ]
console.log(Object.keys(object));

注意到在这个例子中,新添加了三个数字属性,但是最终排序后只有 Math.pow(2,32)-2 符合 array index 的条件被提前,这里的原因又是什么呢?

我们来看下规范对 array index 的定义:

Anarray indexis aninteger indexwhose numeric valueiis in the range +0 ≤i< (2^32- 1).

显然负数和 2^32 -1 (左边区间为 <)不符合 array index 的定义,因此这两个 “数字” 被当做字符串来处理落到了第二个区间,因此输出结果只有 2^32 -2 被提前排序 了。

更多思考

Object.keys 不仅仅可以传入对象获取对象的属性,也可以传入字符串或者数组,这两者均会返回一个索引数组:

// 输出 [ '0', '1', '2' ]
console.log(Object.keys(['a', 'b', 'c']));

// 输出 [ '0', '1', '2' ]
console.log(Object.keys('abc'));

大家有兴趣可以看看规范中是如何定义这两种情况的返回值的。

结语

其实从本文中可以看到,ECMAScript 规范定义的相当严谨,而在 JavaScript 的世界里,不少基础编程上的疑问是类似 “公理” 一样的规范预设的条件,正是在这一些基础预设的条件下,JS 形成了一门图灵完备的语言在各个领域下大施拳脚。

学会解读规范,或者说学会从规范中找到我们钻研编程疑问不断向下碰触到的 “瓶颈”,我觉得对更深入理解 JS 语言本身有很大的好处,甚至更多的时候我们可以通过挖掘某个 SPEC 定义时的原始讨论来看看为什么要这样设计。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8