JavaScript 中的相等性判斷
JavaScript 提供三種不同的比較操作符:
ES2015標準提供以下四種比較操作符:
- (非嚴格)相等 (
==
) - 嚴格相等 (
===
): 用于Array.prototype.indexOf
,Array.prototype.lastIndexOf
, 以及case
語句的匹配操作 - 零值相等: 用于
TypedArray
和ArrayBuffer
的構造、Map
和Set
操作, 并將用于ES2016標準中的String.prototype.includes
- 同值相等: 用于所有其他場景
你可以根據你的需要選擇操作符。
簡單地說,兩等號判等會在比較時進行類型轉換;三等號判等不會進行類型轉換(如果類型不同會直接返回 false ); Object.is
在三等號判等的基礎上特別處理了 NaN
、 -0
和 +0
,保證 -0 和 +0 不再相同,但 Object.is(NaN, NaN)
會返回 true
。 (像其他數值一樣比較 NaN ——由于 IEEE 754 的規范,無論使用雙等號或三等號,比較 NaN 都會得到 false )但請注意,此外,這三個運算符的原語中,沒有一個會比較兩個變量是否結構上概念類似。對于任意兩個不同的非原始對象,即便他們有相同的結構, 以上三個運算符都會計算得到 false 。
嚴格相等 ===
全等操作符比較兩個值是否相等,兩個被比較的值在比較前都不進行隱式轉換。如果兩個被比較的值具有不同的類型,這兩個值是不全等的。否則,如果兩個 被比較的值類型相同,值也相同,并且都不是 number 類型時,兩個值全等。最后,如果兩個值都是 number 類型,當兩個都不是 NaN,并且數值相同,或是兩個值分別為 +0 和 -0 時,兩個值被認為是全等的。
var num = 0;
var obj = new String("0");
var str = "0";
var b = false;
print(num === num); // true
print(obj === obj); // true
print(str === str); // true
print(num === obj); // false
print(num === str); // false
print(obj === str); // false
print(null === undefined); // false
print(obj === null); // false
print(obj === undefined); // false</code></pre>
在日常中使用全等操作符幾乎總是正確的選擇。對于除了數值之外的值,全等操作符使用明確的語義進行比較:一個值只與自身全等。對于數值,全等操作符 使用略加修改的語義來處理兩個特殊情況:第一個情況是,浮點數 0 是不分正負的。區分 +0 和 -0 在解決一些特定的數學問題時是必要的,但是大部分境況下我們并不用關心。全等操作符認為這兩個值是全等的。第二個情況是,浮點數包含了 NaN 值,用來表示某些定義不明確的數學問題的解,例如:正無窮加負無窮。全等操作符認為 NaN 與其他任何值都不全等,包括它自己。(等式 (x !== x
) 成立的唯一情況是 x 的值為 NaN)
非嚴格相等 ==
相等操作符比較兩個值是否相等,在比較前將兩個被比較的值轉換為相同類型。在轉換后(等式的一邊或兩邊都可能被轉換),最終的比較方式等同于全等操作符 === 的比較方式。 相等操作符滿足交換律。
相等操作符對于不同類型的值,進行的比較如下圖所示:
被比較值 B
Undefined
Null
Number
String
Boolean
Object
被比較值 A
Undefined
true
true
false
false
false
IsFalsy(B)
Null
true
true
false
false
false
IsFalsy(B)
Number
false
false
A === B
A === ToNumber(B)
A=== ToNumber(B)
A=== ToPrimitive(B)
String
false
false
ToNumber(A) === B
A === B
ToNumber(A) === ToNumber(B)
ToPrimitive(B) == A
Boolean
false
false
ToNumber(A) === B
ToNumber(A) === ToNumber(B)
A === B
false
Object
false
false
ToPrimitive(A) == B
ToPrimitive(A) == B
ToPrimitive(A) == ToNumber(B)
A === B
在上面的表格中,ToNumber(A)
嘗試在比較前將參數 A 轉換為數字,這與 +A(單目運算符+)的效果相同。通過嘗試依次調用 A 的A.toString 和 A.valueOf 方法,將參數 A 轉換為原始值。
一般而言,根據 ECMAScript 規范,所有的對象都與 undefined
和 null
不相等。但是大部分瀏覽器允許非常窄的一類對象(即,所有頁面中的 document.all
對象),在某些情況下,充當效仿 undefined
的角色。相等操作符就是在這樣的一個背景下。因此,IsFalsy(A)
方法的值為 true
,當且僅當 A
效仿 undefined
。在其他所有情況下,一個對象都不會等于 undefined
或 null
。
var num = 0;
var obj = new String("0");
var str = "0";
var b = false;
print(num == num); // true
print(obj == obj); // true
print(str == str); // true
print(num == obj); // true
print(num == str); // true
print(obj == str); // true
print(null == undefined); // true
// both false, except in rare cases
print(obj == null);
print(obj == undefined);</code></pre>
有些開發者認為,最好永遠都不要使用相等操作符。全等操作符的結果更容易預測,并且因為沒有隱式轉換,全等比較的操作會更快。
同值相等
同值相等解決了最后一個用例:確定兩個值是否在任何情況下功能上是相同的。(這個用例演示了里氏替換原則的實例)當試圖對不可變(immutable)屬性修改時:
// 向 Nmuber 構造函數添加一個不可變的屬性 NEGATIVE_ZERO
Object.defineProperty(Number, "NEGATIVE_ZERO",
{ value: -0, writable: false, configurable: false, enumerable: false });
function attemptMutation(v)
{
Object.defineProperty(Number, "NEGATIVE_ZERO", { value: v });
}</code></pre>
Object.defineProperty
在試圖修改不可變屬性時,如果這個屬性確實被修改了則會拋出異常,反之什么都不會發生。例如如果 v 是 -0 ,那么沒有發生任何變化,所以也不會拋出任何異常。但如果 v 是 +0 ,則會拋出異常。不可變屬性和新設定的值使用 same-value 相等比較。
同值相等由 Object.is
方法提供。
零值相等
與同值相等類似,不過會認為 +0 與 -0 相等。
規范中的相等、嚴格相等以及同值相等
在 ES5 中, ==
相等在 Section 11.9.3, The Abstract Equality Algorithm; ===
相等在 11.9.6, The Strict Equality Algorithm。(請參考這兩個鏈接,他們很簡潔易懂。提示:請先閱讀嚴格相等的算法)ES5 也提供了 same-value 相等, Section 9.12, The SameValue Algorithm ,用在 JS 引擎內部。除了 11.9.6.4 和 9.12.4 在處理數字上的不同外,它基本和嚴格相等算法相同。ES6 簡單地通過 Object.is
暴露了這個算法。
我們可以看到,使用雙等或三等時,除了 11.9.6.1 類型檢查,嚴格相等算法是相等算法的子集因為 11.9.6.2–7 對應 11.9.3.1.a–f。
理解相等比較的模型
在 ES2015 以前,你可能會說雙等和三等是“擴展”的關系。比如有人會說雙等是三等的擴展版,因為他處理三等所做的,還做了類型轉換。例如 6 == "6" 。反之另一些人可能會說三等是雙等的擴展,因為他還要求兩個參數的類型相同,所以增加了更多的限制。怎樣理解取決于你怎樣看待這個問題。
但是這種比較的方式沒辦法把 ES2015 的 Object.is
排列到其中。因為 Object.is
并不比雙等更寬松,也并不比三等更嚴格,當然也不是在他們中間。從下表中可以看出,這是由于 Object.is
處理 NaN
的不同。注意假如 Object.is(NaN, NaN)
被計算成 false
,我們就可以說他比三等更為嚴格,因為他可以區分 -0
和 +0
。但是對 NaN
的處理表明,這是不對的。 Object.is
應該被認為是有其特殊的用途,而不應說他和其他的相等更寬松或嚴格。
判等
x
y
==
===
Object.is
undefined
undefined
true
true
true
null
null
true
true
true
true
true
true
true
true
false
false
true
true
true
"foo"
"foo"
true
true
true
{ foo: "bar" }
x
true
true
true
0
0
true
true
true
+0
-0
true
true
false
0
false
true
false
false
""
false
true
false
false
""
0
true
false
false
"0"
0
true
false
false
"17"
17
true
false
false
[1,2]
"1,2"
true
false
false
new String("foo")
"foo"
true
false
false
null
undefined
true
false
false
null
false
false
false
false
undefined
false
false
false
false
{ foo: "bar" }
{ foo: "bar" }
false
false
false
new String("foo")
new String("foo")
false
false
false
0
null
false
false
false
0
NaN
false
false
false
"foo"
NaN
false
false
false
NaN
NaN
false
false
true
什么時候使用 Object.is
或是三等
總的來說,除了對待NaN
的方式,Object.is
唯一讓人感興趣的,是當你需要一些元編程方案時,它對待0的特殊方式,特別是關于屬性描述器,即你的工作需要去鏡像Object.defineProperty
的一些特性時。如果你的工作不需要這些,那你應該避免使用Object.is
,使用===
來代替。即使你需要比較兩個NaN
使其結果為true
,總的來說編寫使用NaN
檢查的特例函數(用舊版本ECMAScript的isNaN方法
)也會比想出一些計算方法讓Object.is
不影響不同符號的0的比較更容易些。
這里是一個會區別對待-0和+0的內置方法和操作符不完全列表:
顯而易見,對0一元負操作得到
-0
。但表達式的抽象化可能在你沒有意識到得情況下導致-0延續傳播。例如當考慮下例時:
let stoppingForce = obj.mass * -obj.velocity
如果obj.velocity
是0
(或計算結果為0
), 一個-0
就在上處產生并被賦值為stoppingForce的值
.
即使傳入的參數中沒有-0,這些方法的返回值都有可能是-0。例如當用 Math.pow
計算-Infinity
的任何負奇指數的冪都會得到-0
。詳情請參見這些方法各自的文檔。
當傳入參數中有-0時,這些方法也可能返回-0。例如, Math.min(-0, +0)
得出 -0
。詳情請參見這些方法各自的文檔。
這些操作符內部都使用了ToInt32算法。因為內部32位整數類型只有一個0(沒有符號區別),-0的符號在反操作后并不會保留下來。例如Object.is(~~(-0), -0)
和Object.is(-0 << 2 >> 2, -0)
都會得到false
.
在未考慮0的符號的情況下依賴于Object.is
是危險的。當然,如果本意就是區分-0和+0的話,
Object.is
能按照期望完成工作。
參見