JavaScript 中的相等性判斷

吳青強 8年前發布 | 11K 次閱讀 JavaScript開發 JavaScript

JavaScript 提供三種不同的比較操作符:

  • 嚴格相等,使用 ===
  • (非嚴格)相等,使用 ==
  • 以及 Object.is (ECMAScript 6 新特性)

ES2015標準提供以下四種比較操作符:

  • (非嚴格)相等 (==)
  • 嚴格相等 (===): 用于Array.prototype.indexOfArray.prototype.lastIndexOf, 以及 case語句的匹配操作
  • 零值相等: 用于TypedArrayArrayBuffer的構造、MapSet操作, 并將用于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.velocity0 (或計算結果為0), 一個-0就在上處產生并被賦值為stoppingForce的值.

Math.atan2

Math.ceil

Math.pow

Math.round

即使傳入的參數中沒有-0,這些方法的返回值都有可能是-0。例如當用 Math.pow計算-Infinity的任何負奇指數的冪都會得到-0。詳情請參見這些方法各自的文檔。

Math.floor

Math.max

Math.min

Math.sin

Math.sqrt

Math.tan

當傳入參數中有-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能按照期望完成工作。

參見

 本文由用戶 吳青強 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!