• Object.keys的‘诡异’特性,你值得收藏!


    先从‘诡异’的问题入手

    • 例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的规范定义

    1. 调用ToObject(O)将结果赋值给变量obj
    2. 调用EnumerableOwnPropertyNames(obj, "key")将结果赋值给变量nameList
    3. 调用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传的参数都会是一个对象,但我们也可以来看看其它类型值的返回值会是怎样的?

    • Number
    console.log(Object.keys(123)) // []
    

    返回的是空数组,这是因为new Number(123)并没有可提取的属性

    num1.png

    • String
    console.log(Object.keys('123')) // [ '0', '1', '2' ]
    

    字符串之所以返回的不是空数组,是因为new String('123')有可以提取的属性

    str1.png

    第二步:通过转换后的对象获得属性列表properties。(顺序取决于这里)

    对象属性列表是通过 EnumerableOwnPropertyNames 获取的,其中比较重要的是调用对象的内部方法OwnPropertyKeys获得对象的ownKeys(这些内容可以在ECMAScript规范里面找到,就不展开介绍了,我们重点看排序)

    The [[OwnPropertyKeys]] internal method of an ordinary object O takes no arguments. It performs the following steps when called:

    1. Return ! OrdinaryOwnPropertyKeys(O).

    通过上面的介绍,我们可以发现keys的排序取决于 OrdinaryOwnPropertyKeys(O)

    1.png

    翻译过来就是:

    1. 创建一个空的列表用于存放 keys
    2. 将所有合法的数组索引按升序的顺序存入
    3. 将所有字符串类型索引按属性创建时间以升序的顺序存入
    4. 将所有 Symbol 类型索引按属性创建时间以升序的顺序存入
    5. 返回 keys

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

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

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

    1. 先声明一个变量array,值是一个空数组
    2. 循环属性列表,将每个元素添加到array
    3. 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()

    原文首发地址点这里,欢迎大家关注公众号 「前端南玖」,如果你想进前端交流群一起学习,请点这里

    我是南玖,我们下期见!!!

  • 相关阅读:
    112. 路径总和
    德国药品数据库查询系统(含网址)
    JDBC
    The difference between Manhattan distance and Cosine Distance
    ps的一些快捷键
    2022精选最新金融银行面试真题——附带答案
    RP9-变量
    【Try to Hack】php命令执行和代码执行
    LeetCode(30)长度最小的子数组【滑动窗口】【中等】
    无涯教程-JavaScript - INFO函数
  • 原文地址:https://www.cnblogs.com/songyao666/p/16721686.html