先从‘诡异’的问题入手
例1: 纯Number类型的属性const obj = { 1: 1, 6: 6, 3: 3, 2: 2 } console.log('keys', Object.keys(obj)) // ['1', '2', '3', '6']
返回的key为什么自动按照升序排序了?
例2: 纯String类型的属性const obj2 = { a: 'a', c: 'c', f: 'f', b: 'b', } console.log(Object.keys(obj2)) // ['a', 'c', 'f', 'b']
这里为什么又不自动排序了?
看到这里是不是觉得很懵?话不多说,我们先查文档,看看mdn上对 Object.keys 的描述:
Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。
emm,然而它并没有说到底是按哪种顺序返回的。
探索
既然文档上找不到,那我们就一步一步来慢慢研究
Object.keys的polyfill的实现
if (!Object.keys) { Object.keys = (function () { var hasOwnProperty = Object.prototype.hasOwnProperty, hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'), dontEnums = [ 'toString', 'toLocaleString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'constructor' ], dontEnumsLength = dontEnums.length; return function (obj) { if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) throw new TypeError('Object.keys called on non-object'); var result = []; for (var prop in obj) { if (hasOwnProperty.call(obj, prop)) result.push(prop); } if (hasDontEnumBug) { for (var i=0; i < dontEnumsLength; i++) { if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]); } } return result; } })() };
从 Object.keys 的polyfill的实现,我们可以发现它内部其实是用 for...in 来实现的。那我们就可以去查找 for...in 遍历时的顺序规则。然而它也并没有介绍遍历的顺序是怎样的,那么我们就只能去查找 ECMAScript 的规范了。
Object.keys的规范定义
调用 ToObject(O) 将结果赋值给变量 obj
调用 EnumerableOwnPropertyNames(obj, "key") 将结果赋值给变量 nameList
调用 CreateArrayFromList(nameList) 得到最终的结果
第一步:将参数转换成Object(ToObject(O))
因为Object.keys内部会调用 ToObject(O) 方法,所以它不只是可以接受对象参数,还可以接受其它类型的参数,下面这张表就是 ToObject 根据不同类型的值转成Object的映射:
参数类型 结果 Undefined 抛出TypeError Null 抛出TypeError Number 返回一个新的 Number 对象 String 返回一个新的 String 对象 Boolean 返回一个新的 Boolean 对象 Symbol 返回一个新的 Symbol 对象 Object 直接将Object返回我们通常给Object.keys传的参数都会是一个对象,但我们也可以来看看其它类型值的返回值会是怎样的?
Numberconsole.log(Object.keys(123)) // []
返回的是空数组,这是因为new Number(123)并没有可提取的属性
String
console.log(Object.keys('123')) // [ '0', '1', '2' ]
字符串之所以返回的不是空数组,是因为new String('123')有可以提取的属性
第二步:通过转换后的对象获得属性列表properties。
(顺序取决于这里)
对象属性列表是通过 EnumerableOwnPropertyNames 获取的,其中比较重要的是调用对象的内部方法 OwnPropertyKeys 获得对象的 ownKeys (这些内容可以在ECMAScript规范里面找到,就不展开介绍了,我们重点看排序)
The [[OwnPropertyKeys]] internal method of an ordinary object O takes no arguments. It performs the following steps when called:
Return ! OrdinaryOwnPropertyKeys(O) .
通过上面的介绍,我们可以发现keys的排序取决于 OrdinaryOwnPropertyKeys(O)
翻译过来就是:
创建一个空的列表用于存放 keys 将所有 合法的数组索引 按升序的顺序存入 将所有 字符串类型索引 按属性创建时间以升序的顺序存入 将所有 Symbol 类型索引按属性创建时间以升序的顺序存入 返回 keys注意:属性列表 properties 为List类型( List类型 是 ECMAScript规范类型 )
第三步:将List类型的属性列表properties转换为Array得到最终的结果。
将List类型的属性列表转换成Array类型非常简单:
先声明一个变量 array ,值是一个空数组 循环属性列表,将每个元素添加到 array 中 将 array 返回
总结
Object.keys返回的对象属性顺序
将所有 合法 的数组索引按升序排序 将所有字符串类型索引按属性创建时间以升序排序 将所有 Symbol 类型 索引 按属性创建时间以升序排序合法数组索引指的是正整数,负数或者浮点数一律当做字符串处理。严格来说对象属性没有数字类型的,无论是数字还是字符串,都会被当做字符串来处理。
看题
const obj = {} obj[-1] = -1 obj[1] = 1 obj[1.1] = 1.1 obj['2'] = '2' obj['c'] = 'c' obj['b'] = 'b' obj['a'] = 'a' obj[2] = 2 obj[Symbol(1)] = Symbol(1) obj[Symbol('a')] = Symbol('a') obj[Symbol('b')] = Symbol('b') obj['d'] = 'd' console.log(Object.keys(obj))
经过上面对 Object.key 特性的介绍,想必大家都不会再搞错 Object.keys 的输出顺序了吧。
答案:
[ '1', '2', '-1', '1.1', 'c', 'b', 'a', 'd' ]
看到答案很多同学是不是有很多疑问?
如何理解对象属性是正整数还是字符串?
首先我们上面说过合法数组索引指的是正整数,负数或者浮点数一律当做字符串处理。严格来说对象属性没有数字类型的,无论是数字还是字符串,都会被当做字符串来处理。
所以上面只有 1,'2',2 是合法数组索引,但我们知道其实它们都会被转成字符串,所以后面的 2 会将前面的 '2' 覆盖,然后它们按升序排序。然后负数与浮点数一律当做字符串处理按属性创建时间以升序排序。这样就可以得到上面的答案了。
为什么没有Symbol类型?
因为在 EnumerableOwnPropertyNames 的规范中规定了返回值只应包含字符串属性(上面说了数字其实也是字符串)。
我们也可以在MDN上查看关于 Object.getOwnPropertyNames() 的描述。
Object.getOwnPropertyNames() 方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括 Symbol 值作为名称的属性)组成的数组。
所以 Symbol 属性是不会被返回的,如果要返回 Symbol 属性可以用 Object.getOwnPropertySymbols() 。
以上就是Object.keys 诡异特性示例详解的详细内容,更多关于Object.keys 诡异的资料请关注其它相关文章!
查看更多关于Object.keys 诡异特性示例详解的详细内容...