本文作者:Berwin,W3C效能工作組成員,360導航高階前端工程師。Vue.js早期使用者,《深入淺出Vue.js》(正在出版)作者。部落格連結
前幾天一個朋友問了我一個問題:為什麼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
被呼叫時內部都發生了什麼。
1. 答案
對於上面那個問題先給出結論,Object.keys
在內部會根據屬性名key
的型別進行不同的排序邏輯。分三種情況:
- 如果屬性名的型別是
Number
,那麼Object.keys
返回值是按照key
從小到大排序 - 如果屬性名的型別是
String
,那麼Object.keys
返回值是按照屬性被建立的時間升序排序。 - 如果屬性名的型別是
Symbol
,那麼邏輯同String
相同
這就解釋了上面的問題。
下面我們詳細介紹Object.keys
被呼叫時,背後發生了什麼。
2. 當Object.keys
被呼叫時背後發生了什麼
當Object.keys
函式使用引數O
呼叫時,會執行以下步驟:
第一步:將引數轉換成Object
型別的物件。
第二步:通過轉換後的物件獲得屬性列表properties
。
注意:屬性列表
properties
為List型別(List型別是ECMAScript規範型別)
第三步:將List型別的屬性列表properties
轉換為Array得到最終的結果。
規範中是這樣定義的:
- 呼叫
ToObject(O)
將結果賦值給變數obj
- 呼叫
EnumerableOwnPropertyNames(obj, "key")
將結果賦值給變數nameList
- 呼叫
CreateArrayFromList(nameList)
得到最終的結果
2.1 將引數轉換成Object(ToObject(O)
)
ToObject
操作根據下表將引數O
轉換為Object型別的值:
引數型別 | 結果 |
---|---|
Undefined | 丟擲TypeError |
Null | 丟擲TypeError |
Boolean | 返回一個新的 Boolean 物件 |
Number | 返回一個新的 Number 物件 |
String | 返回一個新的 String 物件 |
Symbol | 返回一個新的 Symbol 物件 |
Object | 直接將Object返回 |
因為Object.keys
內部有ToObject
操作,所以Object.keys
其實還可以接收其他型別的引數。
上表詳細描述了不同型別的引數將如何轉換成Object型別。
我們可以簡單寫幾個例子試一試:
先試試null
會不會報錯:
圖1 Object.keys(null)
如圖1所示,果然報錯了。
接下來我們試試數字的效果:
圖2 Object.keys(123)
如圖2所示,返回空陣列。
為什麼會返回空陣列?請看圖3:
圖3 new Number(123)
如圖3所示,返回的物件沒有任何可提取的屬性,所以返回空陣列也是正常的。
然後我們再試一下String的效果:
圖4 Object.keys(`我是Berwin`)
圖4我們會發現返回了一些字串型別的數字,這是因為String物件有可提取的屬性,看如圖5:
圖5 new String(`我是Berwin`)
因為String物件有可提取的屬性,所以將String物件的屬性名都提取出來變成了列表返回出去了。
2.2 獲得屬性列表(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使用,它是一個通用的規則)
2.3 將List型別轉換為Array得到最終結果(CreateArrayFromList( elements )
)
現在我們已經得到了一個物件的屬性列表,最後一步是將List型別的屬性列表轉換成Array型別。
將List型別的屬性列表轉換成Array型別非常簡單:
- 先宣告一個變數
array
,值是一個空陣列 - 迴圈屬性列表,將每個元素新增到
array
中 - 將
array
返回
3. 該順序規則還適用於其他API
上面介紹的排序規則同樣適用於下列API:
Object.entries
Object.values
for...in
迴圈Object.getOwnPropertyNames
Reflect.ownKeys
注意:以上API除了
Reflect.ownKeys
之外,其他API均會將Symbol
型別的屬性過濾掉。