前言
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."