在 JavaScript 中想判斷變數或對象是否相等有以下三種方法:
一般相等(
==
)嚴格相等(
===
)Object.is
()
方法
前兩個比較運算子 ==
和 ===
都可以拿來判斷比較對象是否相等,不過兩者的差別究竟在哪裡?又為什麼要這樣設計?是我一開始學習 JavaScript 感到有點混淆的地方,也是滿常見的面試考題。
==
與 ===
的差別
「大部分情況下不建議使用
==
,應該使用===
」
初學 JavaScript 的我們通常會直接看到這樣的結論,但這是為什麼呢?簡單來說 ==
不會進行型別檢查只會比較值,===
則會同時檢查型別與值。
console.log("1" == 1); // true
console.log("1" === 1); // false
看到這邊可能會覺得 ==
不用管型別簡單明瞭又方便!但其實會因為型別轉型導致出乎意料的結果,即使是 ===
也有一些特殊的例外狀況需要使用 Object.is
()
來解決。
==
等於(Equal) 與 !=
不等於
==
等於(Equal)也稱作「鬆散/寬鬆相等(Loose Equal)」(相較於「嚴格相等」來說)。由於 JavaScript 是個動態型別語言,因此可以允許不同型別的值做比較。此外, ==
也具有「對稱性(symmetric)」, A == B
的意思與 B == A
是相同的。
遇到不同型別的運算元時,會先自動轉型(type conversion)再比較內容。
console.log("1" = 1); //true
console.log(true = 1); // true
不必管型別使用起來看起來方便許多,但⋯⋯事情真的不是憨人所想得這麼簡單!
來看看這幾個有點 tricky 的例子:
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)
Boolean: 運算元都是 true 或都是 false 時才是 true
BigInt:兩個 BigInt 都有相同的值時才是 true
Symbol: 兩個 symbol 都引用相同的 symbol 值時才是 true
Object: 兩個 object 都指向相同的位置時才是 true
要注意,JavaScript 的比較也會因為是不是原始型別(primitive type) 而有所差異。非原始型別(像是 object、array 或 class)比較的基準會是看他們是否指向同一個參考(reference),而不是比較他們的值(value)。
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 會有進一步的說明。
string 或 boolean 會轉為 number
string 或 boolean 被拿來跟 number 比較時,會先透過 Number()
轉為 number,再跟另一個 number 比較。
string 與 number 比較
console.log("" == 0); // true
console.log(" " == 0); // true
console.log("" == " "); // false
上面這幾個例子可以看到:
""
被自動轉型成 0,因此"" == 0
可以看作是0 == 0
,兩值相等所以回傳 true" "
即使包含了空白字元,但經過Number(" ")
自動轉型仍是0
,因此也會回傳 true""
與" "
兩者型別相同(都是 string) 因此不需要轉型、直接比較內容,內容並不相等所以回傳 false。
boolean 與 number 比較
console.log(true == 1); // true
console.log(false == 0); // true
true 被
Number(true)
轉成 1false 被
Number(false)
轉成 0
string 與 boolean 比較
string 與 boolean 都會先轉為 number 再作比較。
console.log('0' == false); // true
'0'
字串透過Number('0')
轉為數字0
false
布林值透過Number(false)
轉為數字0
'0' == false
經過轉型後變成0 == 0
,兩值相同回傳 true
console.log('true' == true); // false
'true'
字串透過Number('true')
因為無法轉型成數字因此結果是NaN
true
布林值透過Number(true)
轉為數字1
'true' == true
經過轉型後變成NaN == 1
--> 只要出現NaN
就回傳 false
null、undefined 的比較
運算元是 null
或 undefined
時的規則如下:
當其中一個運算元是 null
或 undefined
,另一個運算元也必須是 null
或 undefined
才會回傳 true,否則一律回傳 false。
console.log(null == undefined) // true
console.log(null == 0); // false
console.log(undefined == 0); // false
object 與 non-object 的比較
console.log([] == 0); // true
console.log([1] == 1); // true
console.log([] == true); // false ([] 物件透過 .toString() 被轉為 '',再透過 Boolean('') 轉為 false)
上述的例子們拆解後是這樣的:
[] == 0
轉型後 =>0 == 0
=> true- 左邊的物件
[]
透過.toString()
被轉為空字串''
,再透過Number('')
轉為數字0
- 左邊的物件
[1] == 1
轉型後 =>1 == 1
=> true- 左邊的物件
[1]
透過.toString()
被轉為字串'1'
,再透過Number(1)
轉為數字1
- 左邊的物件
[] == true
轉型後 =>0 == 1
=> false左邊的物件
[]
透過.toString()
被轉為空字串''
,再透過Number('')
轉為數字0
右邊的布林值
true
透過Number(true)
被轉為數字1
{}
的比較會回傳 error
console.log({} == true)
// Uncaught SyntaxError: Unexpected token '=='
{}
不能被拿來比對,因為 {}
不代表物件,而是代表一個區塊程式碼。區塊程式碼屬於陳述式,不能拿來做比對。
關於物件被轉型成基本型態,這邊節錄 MDN 裡的說明:
Objects are converted to primitives by calling its @@toPrimitive (with “default” as hint),
valueOf()
, andtoString()
methods, in that order.For
valueOf()
andtoString()
, 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)。
不過這邊找到這個滿有趣的網站,詳細展示了使用 ==
時 JavaScript 是如何 step by step 針對 x == y
進行自動轉型與判斷:https://felix-kling.de/js-loose-comparison/
這邊嘗試當個翻譯蒟蒻並歸納網站的說明:
判斷型別:
Type(x)
與Type(y)
是否相同 --> 若型別相同則回傳嚴格比較的結果:x === y
判斷
null
與undefined
如果
x
是 null、y
是 undefined,回傳 true如果
x
是 undefined、y
是 null,回傳 true
字串或布林值 vs 數字:如果是字串或布林值與數字相比較,會先透過
ToNumber()
將字串或布林值轉型成數字物件 vs 與非物件(字串、數字、Symbol):如果是物件(object)與非物件相比較,會先將物件透過
ToPrimitive()
將物件轉型成基本型態回傳 false
===
嚴格等於(Strict Equal)與 !==
嚴格不等於
顧名思義相較於寬鬆等於(==
)會更嚴謹,===
會先比較型別、再判斷內容是否相等。上面提到幾個在 ==
下會回傳 true 的例子,改用 ===
後結果都是 false:
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
時,會回傳 trueNaN
不等於任何值(包含自己),因此NaN === NaN
會是 false(關於NaN
的詳細說明:NaN (Not a Number))
+0 === -0; // true
NaN === NaN; // false
Object.is
()
同值比較 (same-value equality)
上述的特殊狀況可以在 ES6 改用 Object.is
()
方法來解決。
Object.is(value, value)
Object.is
()
跟 ===
唯一的差別只在於處理帶符號的 0 (例如 +0
、-0
)以及 NaN
值的時候。
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
零值相等
null 和 undefined
null == undefined // true null === undefined // false Object.is(null, undefined) // false
NaN
NaN == NaN // false NaN === NaN // false Object.is(NaN, NaN) // true
0 和帶符號的 0(+0 或 -0)
0 == -0 // true 0 === -0 // true Object.is(0, -0) // false
Reference vs Value
除了轉不轉型,這邊還想要特別補充 JavaScript 的比較基準也會因為型別不同而有所差異。
如果運算元是一般的原始型別(primitive type),例如字串或數字,就是直接比較它們的值是否相等;如果運算元是陣列(array)、物件(object)或 class 等非原始型別時 ,那麼比較的基準就會是看他們是否指向同一個參考(reference),而不是比較他們的值(value)。
// 運算元是陣列
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。
為什麼不是根據內容來判斷是否相等呢?以下直接引用我覺得講得很清楚的這篇「我知道 == 與 === 不同,但為什麼要這樣設計?」的內容。
先定義所謂的「相等性」:
在程式語言當中,相等性通常可以分為三種:
Reference:兩個值指向的指標相同
Shallow Compare:兩個物件的 attributes 長度以及名稱相同,且屬性的 reference 也相同。
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?)
補充
JS Comparison Table (dorey.github.io)
透過這個比較表可以查看各種運算元在==
和===
的結果,以及if
判斷式會將哪些情況視為 true 或 false。