0%

JS 比較運算子的自動轉型

此篇會依照 ECMA-262 11th(ES2020)規則,來深入探討比較運算子轉型的規則。

比較運算子注意事項

在 JavaScript 中除了基礎型別(Primitive Type)以外的型別(ex: 陣列、物件)在做比較時,判斷的是兩者是否指向同一個 reference,而非判斷兩者的值是否相同。

e.g. 陣列判斷

1
2
3
4
5
6
let a = [1 , 2];
let b = [1 , 2];
console.log(a == b); // false

let c = b;
console.log(c == b); // true

下方會在針對其它例外情況做詳細介紹。

==

equality:比較等號兩邊值是否相同,當等號兩邊型別不同時,會先依照規則轉型再進行比對。(除了物件外,幾乎會轉型為 number 後在做比較)

e.g. 基礎 equality

1
console.log('1' == 1); // true

下方會解析 ECMA-262 11th(ES2020)規範:7.2.15 Abstract Equality Comparison 是如何比較的以及轉型的過程。

Step1:先檢查型別,型別相同則回傳 x === y 的結果。

當型別相同,則不需要轉型因此可以直接比較。

1
console.log('b' === 'a'); // false

Step2:等號兩邊為 null 和 undefined 回傳 true。

1
2
console.log(undefined == null); // true
console.log(null == undefined); // true

Step3:基礎型別自動轉成 number 後在做比較。(透過原生函式 ToNumber)

1
2
3
console.log('1' == 1);       // true,1 == 1
console.log(true == '1'); // true,1 == 1
console.log(true == 'true'); // false,1 == NaN

⭐️ Step4:先將非基礎型別轉成基礎型別後再做比較。(透過原生函示 toPrimitive)

toPrimitive 將物件透過原生方法(ex:valueOf、toString)轉成基礎型別。

string 的執行順序

  • 先執行 toString > 還不是 primitive 在執行 valueOf > 還不是 primitive 就噴 TypeError

number 的執行順序

  • (PreferredType 預設為 number)先執行 valueOf > 還不是 primitive 在執行 toString > 還不是 primitive 就噴 TypeError

e.g. 觀察 toPrimitive 中 valueOf 、 toString 優先順序

1
2
3
4
5
6
7
8
9
10
11
12
13
let obj = {
toString : function(){
console.log("toString");
return {};
},
valueOf : function(){
console.log("valueOf");
return {};
}
};

String(obj); // toString -> valueOf -> TypeError
Number(obj); // valueOf -> toString -> TypeError

e.g. toPrimitive 自定義方法,依照 hint 種類輸出結果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let obj = {
name: 'kent',
age: 18,
sex: 'M',
[Symbol.toPrimitive]: function(hint) {
// hint 三種 'string', 'number', 'default'
switch(hint) {
case 'default': return this.sex;
case 'string': return this.name;
case 'number': return this.age;
default: throw new Error();
}
}
}

console.log(obj == 'M'); // true,default
console.log(Number(obj) == 18); // true,number
console.log(String(obj) == 'kent'); // true,string

以上為 == 比較運算子的自動轉型過程。


===

strict equality:比較等號兩邊 型別 以及 數值 是否 都相同

e.g. 基礎 strict equality

1
console.log('1' === 1); // false

下方會解析 ECMA-262 11th(ES2020)規範:7.2.16 Strict Equality Comparison 是如何比較的以及轉型的過程。

Step1:檢查型別,型別不同則回傳 false。

1
console.log('1' === 1); // false

Step2:型別為 number 時會回傳 Number::equal ( x, y ) 結果,型別為 BigInt 時會回傳 BigInt::equal ( x, y ) 結果。

在這之前需要,先暸解 Number、BigInt 兩者的 equal ( x, y ) 規則。

Number::equal ( x, y ) 規則:

  • 有 NaN 就回傳 false。
  • 值相同就回傳 true。
  • 兩者為 0 就回傳 true。(不管正負)
1
2
3
console.log(NaN === NaN); // false
console.log(+1 === -1); // false
console.log(0 === -0); // true

BigInt::equal ( x, y ) 規則:

  • BigInt 值相同回傳 true,否則回傳 false。

根據 MDN 文件:「BigInt 是一個內建的物件,提供了表示大於 “2 的 53 次方” 整數的功能 (“2 的 53 次方” 是 JavaScript 原生的 Number 能夠表示的最大值)」

1
2
console.log(1n === 1n); // true
console.log(1n === 2n); // false

Step3:會依照 SameValueNonNumeric 規則回傳結果。

SameValueNonNumeric ECMA Assert:

  • 兩者不是 Number 或 BigInt 型別。
  • 兩者值不相同。

SameValueNonNumeric 規則:

  • undefined 回傳 true。
  • null 回傳 true。
  • 字串型別時判斷字串是否相同,相同回傳 true 否則回傳 false。
  • booleansymbol 和 object 型別都是比較值是否相同 。(但 object、symbols 還需要比較 reference 是否相同)
1
2
3
4
5
6
7
8
console.log(true === true);           // true
console.log(Symbol(1) === Symbol(1)); // false
console.log({} === {}); // false

const obj = {};
const sym = Symbol();
console.log([obj].includes(obj)); // true
console.log([sym].includes(sym)); // true

conclusion

  • objectsymbols 還需要比較 reference,指向同一個 references 才會回傳 true。
  • === 不會做自動轉型。
  • == 除了物件外,會轉型為 number 在做比較。
  • 判斷是否為 null 、 undefined 可以使用 == 替代 ===
1
2
3
4
5
6
7
8
if (x === null || x === undefined) {
// ...
}

// == 特性等號左右兩邊為 null、undefined 會回傳 true
if (x == null) {
// ...
}

常見迷思

== 不會檢查型別,直接轉型。

== 第一步不是直接轉型,而是先檢查型別,再依造規則做轉型。

分享幾張 == 、 ===、if 的視覺化圖表

src:JavaScript-Equality-Table

=== 比較表

== 比較表

綜合比較表

if/else (綠色代表 true、白色代表 false)

最後一張:Relational and Equality Operators

依照嚴格程度排序不同的比較子(紅色最嚴格、灰色最寬鬆)

最後建議新手一律使用嚴謹的 === 進行比較,避免因為自動轉型而產生非預期的結果,有經驗的就可以自由使用兩者囉。

references

白話文:前端三十|13. [JS] 為什麼判斷相等時不能用雙等號?
簡單解析ES6文件:JavaScript 可愛筆記 #0 – 兩個等於真的有那麼壞嗎?
解析 ToPrimitive:js隐式装箱-ToPrimitive
stackoverflow 討論:Which equals operator (== vs ===) should be used in JavaScript comparisons?