owned this note
owned this note
Published
Linked with GitHub
# JavaScript 的相等比較:`==`、`===` 和 `Object.is()`
在 JavaScript 中想判斷變數或對象是否相等有以下三種方法:
- 一般相等(`==`)
- 嚴格相等(`===`)
- `Object.is()` 方法
前兩個[比較運算子](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Expressions_and_operators#comparison_operators)`==` 和 `===` 都可以拿來判斷比較對象是否相等,不過兩者的差別究竟在哪裡?又為什麼要這樣設計?是我一開始學習 JavaScript 感到有點混淆的地方,也是滿常見的面試考題。
## `==` 與 `===` 的差別
> 「大部分情況下不建議使用 `==`,應該使用 `===`」
初學 JavaScript 的我們通常會直接看到這樣的結論,但這是為什麼呢?簡單來說 **`==` 不會進行型別檢查只會比較值,`===` 則會同時檢查型別與值**。
```javascript
console.log("1" == 1); // true
console.log("1" === 1); // false
```
看到這邊可能會覺得 `==` 不用管型別簡單明瞭又方便!但其實會因為型別轉型導致出乎意料的結果,即使是 `===` 也有一些特殊的例外狀況需要使用 `Object.is()` 來解決。
## `==` 等於(Equal) 與 `!=` 不等於
`==` 等於(Equal)也稱作「鬆散/寬鬆相等(Loose Equal)」(相較於「嚴格相等」來說)。由於 JavaScript 是個[動態型別語言](https://hackmd.io/tW7ORGtMTCmKHtm8lA2WGA#Dynamic-amp-Weak-Typing),因此可以允許不同型別的值做比較。此外, `==` 也具有「對稱性(symmetric)」, `A == B` 的意思與 `B == A` 是相同的。
遇到不同型別的運算元時,會**先自動轉型(type conversion)再比較內容**。
```javascript
console.log("1" = 1); //true
console.log(true = 1); // true
```
不必管型別使用起來看起來方便許多,但⋯⋯事情真的不是憨人所想得這麼簡單!
來看看這幾個有點 tricky 的例子:
```javascript
console.log(-0 == +0);
console.log(null == undefined);
console.log([1,2] == "1,2");
console.log([] == false);
console.log([0] == false);
console.log("\n" == false);
console.log("0XA19" == 2585);
```
以上的結果竟然都是 true!
到這邊你可能會跟我一樣頭痛。原來不同型別的值經過 **JavaScript 的隱式轉型(Implicit Type Coercion)** 反而會有預期之外的結果。
這也是為什麼通常來說更建議使用 `===`。(是不是突然又覺得 `===` 比較簡單了...)
以下來做個簡單的分類:
### 同型別之間的比較
如果 `Type(x)` 與 `Type(y)` 相同,執行的結果就跟使用嚴格相等 `x === y` 一樣。
- String: 兩個 string 每一個字元跟順序都相同時才是 true
- Number: 兩個 number 的值相同時才是 true
- `+0` 與 `-0` 在 `==` 被視為相等、在 `===` 視為不相等
- 只要任一運算元是 `NaN` 就是 false,`NaN` 不等於任何值(包含自己)(Ref: [Javascript - NaN(Not a Number)](/9FVpL8cLTM2jH6tBvUkwUA)
- Boolean: 運算元都是 true 或都是 false 時才是 true
- BigInt:兩個 BigInt 都有相同的值時才是 true
- Symbol: 兩個 symbol 都引用相同的 symbol 值時才是 true
- Object: 兩個 object 都指向相同的位置時才是 true
:::info
要注意,JavaScript 的比較也會因為是不是原始型別(primitive type) 而有所差異。非原始型別(像是 object、array 或 class)比較的基準會是看他們**是否指向同一個參考(reference),而不是比較他們的值(value)**。
```javascript
let obj1 = { name: 'John' }
let obj2 = { name: 'John' }
let array1 = [1,2,3]
let array2 = [1,2,3]
console.log(obj1 == obj2); // false
console.log(array1 == array2); // false
```
這部分在下方 [Reference vs Value](#Reference-vs-Value) 會有進一步的說明。
:::
### string 或 boolean 會轉為 number
string 或 boolean 被拿來跟 number 比較時,會先透過 `Number()` 轉為 number,再跟另一個 number 比較。
#### string 與 number 比較
```javascript
console.log("" == 0); // true
console.log(" " == 0); // true
console.log("" == " "); // false
```
上面這幾個例子可以看到:
1. `""` 被自動轉型成 0,因此 `"" == 0` 可以看作是 `0 == 0` ,兩值相等所以回傳 true
2. `" "` 即使包含了空白字元,但經過 `Number(" ")`自動轉型仍是 `0` ,因此也會回傳 true
3. `""` 與 `" "` 兩者型別相同(都是 string) 因此不需要轉型、直接比較內容,內容並不相等所以回傳 false。
#### boolean 與 number 比較
```javascript
console.log(true == 1); // true
console.log(false == 0); // true
```
- true 被 `Number(true)` 轉成 1
- false 被 `Number(false)` 轉成 0
#### string 與 boolean 比較
string 與 boolean 都會先轉為 number 再作比較。
```javascript
console.log('0' == false); // true
```
1. `'0'` 字串透過 `Number('0')` 轉為數字 `0`
2. `false` 布林值透過 `Number(false)` 轉為數字 `0`
3. `'0' == false` 經過轉型後變成 `0 == 0` ,兩值相同回傳 true
```javascript
console.log('true' == true); // false
```
1. `'true'` 字串透過 `Number('true')` 因為無法轉型成數字因此結果是 `NaN`
2. `true` 布林值透過 `Number(true)` 轉為數字 `1`
3. `'true' == true` 經過轉型後變成 `NaN == 1` --> 只要出現 `NaN` 就回傳 false
### null、undefined 的比較
運算元是 `null` 或 `undefined` 時的規則如下:
當其中一個運算元是 `null` 或 `undefined`,另一個運算元也必須是 `null` 或 `undefined` 才會回傳 true,否則一律回傳 false。
```javascript
console.log(null == undefined) // true
console.log(null == 0); // false
console.log(undefined == 0); // false
```
### object 與 non-object 的比較
```javascript
console.log([] == 0); // true
console.log([1] == 1); // true
console.log([] == true); // false ([] 物件透過 .toString() 被轉為 '',再透過 Boolean('') 轉為 false)
```
上述的例子們拆解後是這樣的:
1. `[] == 0` 轉型後 => `0 == 0` => true
- 左邊的物件 `[]` 透過 `.toString()` 被轉為空字串 `''`,再透過 `Number('')` 轉為數字 `0`
2. `[1] == 1` 轉型後 => `1 == 1` => true
- 左邊的物件 `[1]` 透過 `.toString()` 被轉為字串 `'1'`,再透過 `Number(1)` 轉為數字 `1`
4. `[] == true` 轉型後 => `0 == 1` => false
- 左邊的物件 `[]` 透過 `.toString()` 被轉為空字串 `''`,再透過 `Number('')` 轉為數字 `0`
- 右邊的布林值 `true` 透過 `Number(true)` 被轉為數字 `1`
#### `{}` 的比較會回傳 error
```javascript
console.log({} == true)
// Uncaught SyntaxError: Unexpected token '=='
```
`{}` 不能被拿來比對,因為 `{}` 不代表物件,而是代表一個區塊程式碼。區塊程式碼屬於陳述式,不能拿來做比對。
:::info
關於物件被轉型成基本型態,這邊節錄 [MDN 裡的說明](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#type_coercion):
> Objects are converted to primitives by calling its [@@toPrimitive]() (with "default" as hint), `valueOf()`, and `toString()` methods, in that order.
> For `valueOf()` and `toString()`, if one returns an object, the return value is ignored and the other's return value is used instead; if neither is present, or neither returns a primitive, a TypeError is thrown.
我們可以得知物件轉換成基本型態的步驟為:先 `valueOf()` → 返回 object → 再 `toString()`
:::
### == 的隱式轉型(Implicit Type Coercion)
關於 Javascript 的轉型(Coercion)一查下去又是另一個大坑,決定另起一篇來談。[Javascript 的自動轉型(Coercion)](/3FpUUDKFSzKGFJCD4QzNnA)。
不過這邊找到這個滿有趣的網站,詳細展示了使用 `==` 時 JavaScript 是如何 step by step 針對 `x == y` 進行自動轉型與判斷:https://felix-kling.de/js-loose-comparison/
這邊嘗試當個翻譯蒟蒻並歸納網站的說明:
1. 判斷型別:`Type(x)` 與 `Type(y)` 是否相同 --> 若型別相同則回傳嚴格比較的結果: `x === y`
2. 判斷 **`null` 與 `undefined`**
- 如果 `x` 是 null、 `y` 是 undefined,回傳 true
- 如果 `x` 是 undefined、 `y` 是 null,回傳 true
4. **字串或布林值 vs 數字**:如果是字串或布林值與數字相比較,會先透過 `ToNumber()` 將字串或布林值轉型成數字
5. **物件 vs 與非物件(字串、數字、Symbol)**:如果是物件(object)與非物件相比較,會先將物件透過 `ToPrimitive()`將物件轉型成基本型態
6. 回傳 false
## `===` 嚴格等於(Strict Equal)與 `!==` 嚴格不等於
顧名思義相較於寬鬆等於(`==`)會更嚴謹,`===` 會**先比較型別、再判斷內容**是否相等。上面提到幾個在 `==` 下會回傳 true 的例子,改用 `===` 後結果都是 false:
```javascript
console.log("" === 0); // false
console.log(true === 1); // false
console.log(false === 0); // false
console.log(null === undefined); // false
console.log([1] === 1); // false
```
### `===` 的例外情況
BUT! ~~人生中最厲害就是這個 BUT!~~ 其實 `===` 有兩種特殊的例外情況:
- 比較 `+0` 與 `-0` 時,會回傳 true
- `NaN` 不等於任何值(包含自己),因此 `NaN === NaN` 會是 false(關於 `NaN` 的詳細說明:[NaN (Not a Number)](/9FVpL8cLTM2jH6tBvUkwUA))
```javascript
+0 === -0; // true
NaN === NaN; // false
```
## `Object.is()` 同值比較 (same-value equality)
上述的特殊狀況可以在 ES6 改用 `Object.is()` 方法來解決。
```javascript
Object.is(value, value)
```
`Object.is()` 跟 `===` 唯一的差別只在於處理帶符號的 0 (例如 `+0`、`-0`)以及 `NaN` 值的時候。
```javascript
console.log(Object.is(+0, -0)); // false
console.log(Object.is(NaN, NaN)); // true
```
`Object.is()` 在以下幾種情況成立時會返回 true:
- 都是 `undefined`
- 都是 `null`
- 都是 `true` 或都是 `false`
- 都是長度相同、字符相同、順序相同的字串
- 都指向相同的位置
- 都是 BigInt 且具有相同的數值
- 都是 symbol 且引用相同的 symbol 值
- 都是數字且
- 都是 `+0` 或都是 `-0`
- 都是 `NaN`
- 都有相同的值,並且非 `0` 也非 `NaN`
## 零值相等
1. null 和 undefined
```javascript
null == undefined // true
null === undefined // false
Object.is(null, undefined) // false
```
2. NaN
```javascript
NaN == NaN // false
NaN === NaN // false
Object.is(NaN, NaN) // true
```
3. 0 和帶符號的 0(+0 或 -0)
```javascript
0 == -0 // true
0 === -0 // true
Object.is(0, -0) // false
```
## Reference vs Value
除了轉不轉型,這邊還想要特別補充 JavaScript 的比較基準也會因為型別不同而有所差異。
如果運算元是一般的**原始型別(primitive type)**,例如字串或數字,就是直接**比較它們的值是否相等**;如果運算元是陣列(array)、物件(object)或 class 等**非原始型別**時 ,那麼比較的基準就會是看他們**是否指向同一個參考(reference),而不是比較他們的值(value)**。
```javascript
// 運算元是陣列
var a = [1,2,3,4,5]
var b = [1,2,3,4,5]
var c = a
console.log(a == b) // false
console.log(a === b) // false
console.log(Object.is(a, b)) // false
console.log(a === c) // true
// 運算元是物件
var obj1 = { name: 'John', age: 18 }
var obj2 = { name: 'John', age: 18 }
var obj3 = obj1
console.log(obj1 == obj2) // false
console.log(obj1 === obj2) // false
console.log(Object.is(obj1, obj2)) // false
console.log(obj1 === obj3) // true
```
內容一模一樣的 array 跟 object 不管用 `==`、`===` 還是 `Object.is()` 統統都是 false。
只有在兩者指向同一位置的時候(`var c = a` 或 `obj1 = obj3`),才會讓兩者在進行相等比較的時候是 true。
為什麼不是根據內容來判斷是否相等呢?以下直接引用我覺得講得很清楚的這篇「[我知道 == 與 === 不同,但為什麼要這樣設計?](https://f2e.kalan.dev/javascript-basic/4.html#reference-v-s-value)」的內容。
先定義所謂的「相等性」:
> 在程式語言當中,相等性通常可以分為三種:
> 1. Reference:兩個值指向的指標相同
> 2. Shallow Compare:兩個物件的 attributes 長度以及名稱相同,且屬性的 reference 也相同。
> 3. Deep Equal:兩個物件的 attibutes 長度、名稱相同,值也相同,如果有巢狀結構,會遞迴持續比較。
如果不用指標、而是想要進一步比較物件裡面的內容,需要定義要比較到「多深」(物件可能有好幾層,也可能遇到 circular reference 的問題),是要 shallow compare 還是要 deep equal?Deep equal 又可能會遇到效能問題。
因此:
> 如果不用指標(reference)來判斷相等性,程式語言不知道要怎麼幫你判斷兩個物件是否相等
> 最簡單也最有效的方式就是直接用 reference 判斷
至於要如何做 Shallow Compare 或 Deep Equal 呢?一般來說會自己寫函式來定義到底要比較到多深,或使用 `JSON.stringify()` 來比較。(延伸閱讀:[How to compare objects in JavaScript?](https://codedamn.com/news/javascript/compare-objects-in-javascript))
## 補充
[**JS Comparison Table (dorey.github.io)**](https://dorey.github.io/JavaScript-Equality-Table/)
透過這個比較表可以查看各種運算元在 `==` 和 `===` 的結果,以及 `if` 判斷式會將哪些情況視為 true 或 false。
[**MDN 的 equality methods 比較表**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness#comparing_equality_methods)
![](https://hackmd.io/_uploads/S1D3Hi9JT.png)
## Ref
- [我知道 `==` 與 `===` 不同,但為什麼要這樣設計?](https://f2e.kalan.dev/javascript-basic/4.html#reference-v-s-value)
- [在 JavaScript 當中,==、=== 與 Object.is()的區別](https://www.explainthis.io/zh-hant/swe/js-equality)
- [Equality comparisons and sameness](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness)
- [JavaScript 核心篇 學習筆記: Chap.24 — 寬鬆相等、嚴格相等以及隱含轉型](https://medium.com/@yining1204/javascript-%E6%A0%B8%E5%BF%83%E7%AF%87-%E5%AD%B8%E7%BF%92%E7%AD%86%E8%A8%98-chap-24-%E5%AF%AC%E9%AC%86%E7%9B%B8%E7%AD%89-%E5%9A%B4%E6%A0%BC%E7%9B%B8%E7%AD%89%E4%BB%A5%E5%8F%8A%E9%9A%B1%E5%90%AB%E8%BD%89%E5%9E%8B-f0794db394c8)