前言
2020 秋天,我將用 30 天的時間,來嘗試回答和網路前端開發相關的 30 個問題。30 天無法一網打盡浩瀚的前端知識,有些問題可能對有些讀者來說相對簡單,不過期待這趟旅程,能幫助自己、也幫助讀者打開不同的知識大門。有興趣的話,跟著我一起探索吧!
今天要來談談關於 JavaScript abstract equality comparison 的事情。我們都知道在 JavaScript 當中,在做比較的時候,有 ==
和 ===
之分,分別代表 abstract equality comparison
和 strict equality comparison
strict equality comparison
會同時比較左右兩邊運算元的「型別」和「值」,如果只要有一個不同,那麼就會回傳 false。
譬如
1 === '1' // false
但如果使用 abstract equality comparison
則就有不同的結果:
1 == '1' // true
因為 JavaScript 會「很聰明」的幫我們轉型,把兩邊的運算元轉換成可以比較的型別與值。
不過,JavaScript 究竟是根據什麼規則進行轉換的呢?
其實規則都寫在 ECMA 的規格文件 當中了:
今天,就讓我們一起好好看一下吧!
在進行 x == y
的比較時:
1
如果 x 和 y 的 type 都一樣,那麼其實就跟操作 strict equality comparision
一樣,不需要進行強制轉型。譬如
1 === 1 // true
'hello' === 'world' // false
2
如果 x 為 null 而 y 為 undefined,回傳 true
null == undefined // true
3
如果 x 為 undefined 而 y 為 null,回傳 true
undefined == null // true
4
如果 x 是 number 而 y 是 string,那麼會將 y 轉成 number 然後再進行比較
1 == '1' // true
2 == '1' // false
5
如果 x 是 string 而 y 是 number,那麼會將 x 轉成 number 然後再進行比較
'1' == 1 // true
'2' == 1 // false
6
如果 x 是 BigInt 而 y 是 string,那麼會將 y 轉成 BigInt 然後再進行比較。如果轉換的過程中出現 NaN,則直接回傳 false
1 == '1' // true
2 == '1' // false
7
如果 x 是 string 而 y 是 BigInt,則回傳 y == x 的結果(參考上面的規定)
'1' == 1 // true
'2' == 1 // false
8
如果 x 是一個 boolean 值,那麼會將 x 轉回 number 後再進行比較
true == 0 // false
true == 1 // true
true == 2 // false
如果這時候 y 是字串,那麼就會參考第四點來做比較
true == '0' // false
true == '1' // true
true == '2' // false
9
如果 y 是一個 boolean 值,則回傳 y == x 的結果(參考上面的規定)
0 == true // false
1 == true // true
2 == true // false
'0' == true // false
'1' == true // true
'2' == true // false
10
如果 x 是 String, Number, BigInt, 或是 Symbol,而 y 是個 Object,那麼會需要先把 y 轉換成 Primative (ToPrimitive(y)
) 之後再進行比較。
但是,ToPrimitive(y)
會變成什麼東西呢?
如果有特別設定 hint
,那麼就會使用該 hint
,譬如是number
或 string
。如果沒有設定 hint
,就會使用 default
。
知道 hint
是什麼之後,接下來,就會開始進行轉換。在實作上,會使用 @@toPrimitive
API 接口的方式,像下面這樣:
const obj = {
[Symbol.toPrimitive](hint) {
if (hint == 'number') {
return 10;
}
if (hint == 'string') {
return 'hello';
}
return true;
}
};
根據 ECMA 文件說明,會先看看即將要轉換的 object,是否有轉換的 hint
,也就是告訴大家該 object 該如何轉型。
如果根據 hint
而得到的轉換結果「不是 undefined
」,本身不是 object,那麼就直接回傳結果。如果轉換結果還是 object,那麼就會丟出 TypeError。
而 hint
實際上來自於 "需要轉換環境",譬如以剛剛的 obj
來說
console.log(+obj) // 10 -- hint is "number"
console.log(`${obj}`) // "hello" -- hint is "string"
console.log(obj + '') // "true" -- hint is "default"
Number(obj) // 10 -- hint is "number"
String(obj) // "hello" -- hint is "string"
如果根據 hint
而得到的轉換結果「是 undefined
」,那麼這時候就需要額外做以下的處理。
首先,如果原本的 hint
是 default
,那麼這時候就會被強制轉成 number
。如果 hint
是
string
,那麼就會去找這個 object 裡面是否有 toString()
或是 valueOf
的方法,之後依序提取。number
,那麼就會去找這個 object 裡面是否有 valueOf
或是 toString()
的方法,之後依序提取。兩種狀況看起來很像,差別在於不同方法的提取順序。之後,就會「依照順序」呼叫方法,如果得到的結果不是 object,就會回傳結果;若還是 object,則會丟出 TypeError。
舉例來說,這裡我們使用原生的陣列 (沒有使用者定義的 @@toPrimitive
):
let arr = [1]
console.log(+arr) // 1 -- hint is "number"
console.log(`${arr}`) // "1" -- hint is "string"
Number(arr) // 1 -- hint is "number"
String(arr) // "1" -- hint is "string"
比較特別的是
console.log(arr + '') // "1" -- hint is "default"
因為 hint
為 default
,所以會被自動轉為 number
,接著,就會依序呼叫 valueOf
和 toString
方法。結果分別為
[1].valueOf() // [1] object
[1].toString() // "1" string
因此最後會回傳 string
的結果。
如果改成 let arr = [1, 2]
的話,最後就無法順利轉成 Primitive 囉!
11
如果 x 是 object 而 y 是 String, Number, BigInt, 或是 Symbol,那麼就會直接回傳 y == x 的結果(參考上面的規則)
12
如果 x 和 y 其中一個為 BigInt 另外一個為 numebr,只要其中有一個是 NaN
, 無限大或無限小,直接回傳 false。如果沒有,則會比較實際上的數值大小,然後回傳結果
13
如果沒有符合上述的任何一個規則,直接回傳 false。譬如我們在規則 10 的時候,轉換過程中出現 TypeError (例外狀況),那麼就會在這裡直接回傳 false。譬如
閱讀原始碼或文件,就可以理解發明者是如何「設計」規則與工具。不管覺得有理還是無理,但這些規則基本上就是默默主宰著世界的運作。
我是覺得很有趣啦,你覺得呢?
TD
Be curious as astronomer, think as physicist, hack as engineer, fight as baseball player"Life is like riding a bicycle. To keep your balance, you must keep moving."