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()

原文首發地址點這裏,歡迎大家關注公衆號 「前端南玖」,如果你想進前端交流羣一起學習,請點這裏

我是南玖,我們下期見!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章