相等比較和相同性

JavaScript 提供三種不同的值比較操作。

  • === — 嚴格相等(三個等號)
  • == — 鬆散相等(兩個等號)
  • Object.is()

您選擇的操作取決於您要執行的比較型別。簡而言之

  • 雙等號 (==) 在比較兩個事物時會執行型別轉換,並且會專門處理 NaN-0+0 以符合 IEEE 754(因此 NaN != NaN,並且 -0 == +0);
  • 三等號 (===) 會執行與雙等號相同的比較(包括對 NaN-0+0 的特殊處理),但不進行型別轉換;如果型別不同,則返回 false
  • Object.is() 不會進行型別轉換,也不會對 NaN-0+0 進行特殊處理(使其與 === 的行為相同,除了這些特殊數值)。

它們對應於 JavaScript 中四種相等演算法中的三種。

請注意,這些之間的區別都與它們對基本型別的處理有關;它們都沒有比較引數在結構上是否概念上相似。對於任何具有相同結構但本身是不同物件的非基本物件 xy,上述所有形式都將評估為 false

使用 === 的嚴格相等

嚴格相等比較兩個值是否相等。在比較之前,兩個值都不會隱式轉換為其他值。如果兩個值具有不同的型別,則這兩個值被認為不相等。如果兩個值具有相同的型別,並且不是數字,並且具有相同的值,則它們被認為相等。最後,如果兩個值都是數字,如果它們都不同於 NaN 並且具有相同的值,或者如果一個是 +0 並且另一個是 -0,則它們被認為相等。

js
const num = 0;
const obj = new String("0");
const str = "0";

console.log(num === num); // true
console.log(obj === obj); // true
console.log(str === str); // true

console.log(num === obj); // false
console.log(num === str); // false
console.log(obj === str); // false
console.log(null === undefined); // false
console.log(obj === null); // false
console.log(obj === undefined); // false

嚴格相等幾乎總是要使用的正確比較操作。對於除數字之外的所有值,它使用明顯的語義:一個值只等於它自己。對於數字,它使用略有不同的語義來掩蓋兩個不同的邊緣情況。第一個是浮點零可以是正號或負號。這在表示某些數學解時很有用,但是由於大多數情況都不關心 +0-0 之間的差異,嚴格相等將它們視為相同的值。第二個是浮點包括一個非數字值的的概念,NaN,用於表示某些定義不明確的數學問題的解:例如,負無窮大加正無窮大。嚴格相等將 NaN 視為與所有其他值(包括它自己)不相等。((x !== x)true 的唯一情況是 xNaN。)

除了 === 之外,嚴格相等還用於包括 Array.prototype.indexOf()Array.prototype.lastIndexOf()TypedArray.prototype.indexOf()TypedArray.prototype.lastIndexOf()case 匹配在內的陣列索引查詢方法。這意味著您不能使用 indexOf(NaN) 來查詢陣列中 NaN 值的索引,或者在 switch 語句中使用 NaN 作為 case 值並使其與任何內容匹配。

js
console.log([NaN].indexOf(NaN)); // -1
switch (NaN) {
  case NaN:
    console.log("Surprise"); // Nothing is logged
}

使用 == 的鬆散相等

鬆散相等是對稱的:對於 AB 的任何值,A == B 的語義始終與 B == A 相同(除了應用轉換的順序)。使用 == 執行鬆散相等的行為如下

  1. 如果運算元具有相同的型別,則它們將按如下方式比較
    • 物件:僅當兩個運算元引用同一個物件時才返回 true
    • 字串:僅當兩個運算元具有相同字元並按相同順序排列時才返回 true
    • 數字:僅當兩個運算元具有相同的值時才返回 true+0-0 被視為相同的值。如果任一運算元是 NaN,則返回 false;因此 NaN 從不等於 NaN
    • 布林值:僅當運算元均為 true 或均為 false 時才返回 true
    • BigInt:僅當兩個運算元具有相同的值時才返回 true
    • Symbol:僅當兩個運算元引用同一個符號時才返回 true
  2. 如果其中一個運算元是 nullundefined,則另一個也必須是 nullundefined 才能返回 true。否則返回 false
  3. 如果其中一個運算元是物件,而另一個是基本型別,則將物件轉換為基本型別
  4. 在此步驟中,兩個運算元都將轉換為基本型別(其中之一是字串、數字、布林值、符號和 BigInt)。其餘轉換將逐案進行。
    • 如果它們是相同的型別,則使用步驟 1 對它們進行比較。
    • 如果其中一個運算元是 Symbol,但另一個不是,則返回 false
    • 如果其中一個運算元是布林值,但另一個不是,則將布林值轉換為數字: true 轉換為 1,false 轉換為 0。然後再次鬆散地比較兩個運算元。
    • 數字到字串:將字串轉換為數字。轉換失敗會導致 NaN,這將保證相等性為 false
    • 數字到 BigInt:透過它們的數值進行比較。如果數字是 ±Infinity 或 NaN,則返回 false
    • 字串到 BigInt:使用與 BigInt() 建構函式相同的演算法將字串轉換為 BigInt。如果轉換失敗,則返回 false

傳統上,根據 ECMAScript 的規定,所有原始型別和物件都與 `undefined` 和 `null` 鬆散不等。但大多數瀏覽器允許一類非常狹窄的物件(具體來說,對於任何頁面,`document.all` 物件),在某些情況下,可以像它們模擬 `undefined` 值一樣工作。鬆散相等就是一個這樣的上下文:`null == A` 和 `undefined == A` 當且僅當 A 是模擬 `undefined` 的物件時才計算為真。在所有其他情況下,物件永遠不會鬆散等於 `undefined` 或 `null`。

在大多數情況下,不建議使用鬆散相等。使用嚴格相等進行比較的結果更容易預測,並且由於沒有型別強制轉換,因此可能執行得更快。

以下示例演示了涉及數字原始型別 `0`、bigint 原始型別 `0n`、字串原始型別 `'0'` 以及 `toString()` 值為 `'0'` 的物件的鬆散相等比較。

js
const num = 0;
const big = 0n;
const str = "0";
const obj = new String("0");

console.log(num == str); // true
console.log(big == num); // true
console.log(str == big); // true

console.log(num == obj); // true
console.log(big == obj); // true
console.log(str == obj); // true

鬆散相等僅由 `==` 運算子使用。

使用 Object.is() 的相同值相等

相同值相等確定兩個值在所有上下文中是否功能相同。(此用例演示了 Liskov 替換原則 的一個例項。)當嘗試修改不可變屬性時,會發生這種情況。

js
// Add an immutable NEGATIVE_ZERO property to the Number constructor.
Object.defineProperty(Number, "NEGATIVE_ZERO", {
  value: -0,
  writable: false,
  configurable: false,
  enumerable: false,
});

function attemptMutation(v) {
  Object.defineProperty(Number, "NEGATIVE_ZERO", { value: v });
}

Object.defineProperty 在嘗試更改不可變屬性時會丟擲異常,但如果未請求實際更改,則不會執行任何操作。如果 `v` 為 `-0`,則未請求更改,不會丟擲錯誤。在內部,當重新定義不可變屬性時,會使用相同值相等將新指定的值與當前值進行比較。

相同值相等由 Object.is 方法提供。它幾乎在需要具有等效標識的值的語言中的所有地方使用。

相同值零相等

類似於相同值相等,但 +0 和 -0 被認為是相等的。

相同值零相等未作為 JavaScript API 公開,但可以使用自定義程式碼實現。

js
function sameValueZero(x, y) {
  if (typeof x === "number" && typeof y === "number") {
    // x and y are equal (may be -0 and 0) or they are both NaN
    return x === y || (x !== x && y !== y);
  }
  return x === y;
}

相同值零僅透過將 `NaN` 視為等效值與嚴格相等不同,並且僅透過將 `-0` 視為等效於 `0` 與相同值相等不同。這使其在搜尋期間通常具有最合理的行為,尤其是在使用 `NaN` 時。它被 Array.prototype.includes()TypedArray.prototype.includes() 以及 MapSet 方法用於比較鍵相等。

比較相等方法

人們經常透過說一個是對另一個的“增強”版本來比較雙等號和三等號。例如,可以將雙等號說成是三等號的擴充套件版本,因為前者執行後者所做的一切,但對其運算元進行型別轉換——例如,`6 == "6"`。或者,可以聲稱雙等號是基線,而三等號是增強版本,因為它要求兩個運算元型別相同,因此它添加了一個額外的約束。

但是,這種思維方式意味著相等比較形成一個一維“頻譜”,其中“完全嚴格”位於一端,“完全鬆散”位於另一端。這個模型不足以說明 Object.is,因為它既不像雙等號那樣“鬆散”,也不像三等號那樣“嚴格”,也不適合介於兩者之間(即,既比雙等號更嚴格,但比三等號更鬆散)。我們可以從下面的相同性比較表中看到,這是由於 Object.is 處理 NaN 的方式造成的。請注意,如果 `Object.is(NaN, NaN)` 計算為 `false`,那麼我們 *可以* 說它適合鬆散/嚴格頻譜,作為三等號的一種更嚴格的形式,一種能夠區分 `-0` 和 `+0` 的形式。然而,NaN 處理意味著這是不正確的。不幸的是,Object.is 必須根據其特定特性來考慮,而不是根據其相對於相等運算子的鬆散或嚴格性。

x y == === Object.is SameValueZero
undefined undefined ✅ true ✅ true ✅ true ✅ true
null null ✅ true ✅ true ✅ true ✅ true
true true ✅ true ✅ true ✅ true ✅ true
false false ✅ true ✅ true ✅ true ✅ true
'foo' 'foo' ✅ true ✅ true ✅ true ✅ true
0 0 ✅ true ✅ true ✅ true ✅ true
+0 -0 ✅ true ✅ true ❌ false ✅ true
+0 0 ✅ true ✅ true ✅ true ✅ true
-0 0 ✅ true ✅ true ❌ false ✅ true
0n -0n ✅ true ✅ true ✅ true ✅ true
0 false ✅ true ❌ false ❌ false ❌ false
"" false ✅ true ❌ false ❌ false ❌ false
"" 0 ✅ true ❌ false ❌ false ❌ false
'0' 0 ✅ true ❌ false ❌ false ❌ false
'17' 17 ✅ true ❌ false ❌ false ❌ false
[1, 2] '1,2' ✅ true ❌ false ❌ false ❌ false
new String('foo') 'foo' ✅ true ❌ false ❌ false ❌ false
null undefined ✅ true ❌ false ❌ false ❌ false
null false ❌ false ❌ false ❌ false ❌ false
undefined false ❌ false ❌ false ❌ false ❌ false
{ foo: 'bar' } { foo: 'bar' } ❌ false ❌ false ❌ false ❌ false
new String('foo') new String('foo') ❌ false ❌ false ❌ false ❌ false
0 null ❌ false ❌ false ❌ false ❌ false
0 NaN ❌ false ❌ false ❌ false ❌ false
'foo' NaN ❌ false ❌ false ❌ false ❌ false
NaN NaN ❌ false ❌ false ✅ true ✅ true

何時使用 Object.is() 與三等號

通常,只有在追求某些超程式設計方案(尤其是在屬性描述符方面)時,Object.is 對零的特殊行為才可能引起關注,當希望你的工作反映 Object.defineProperty 的一些特性時。如果你的用例不需要這樣做,建議避免使用 Object.is,而使用 ===。即使你的需求涉及使兩個 NaN 值之間的比較計算為 `true`,通常對 NaN 檢查進行特殊情況處理(使用從以前版本的 ECMAScript 中提供的 isNaN 方法)比弄清楚周圍的計算如何影響你比較中遇到的任何零的符號要容易得多。

以下是非詳盡的內建方法和運算子列表,這些方法和運算子可能會導致 `-0` 和 `+0` 之間的區別在你的程式碼中顯現出來。

-(一元否定)

考慮以下示例

js
const stoppingForce = obj.mass * -obj.velocity;

如果 `obj.velocity` 為 `0`(或計算為 `0`),則在該位置引入一個 `-0`,並傳播到 `stoppingForce` 中。

Math.atan2Math.ceilMath.powMath.round

在某些情況下,即使沒有 `-0` 作為引數之一,也可能將 `-0` 作為這些方法的返回值引入表示式中。例如,使用 Math.pow-Infinity 提升到任何負奇數指數的冪,計算結果為 `-0`。請參閱各個方法的文件。

Math.floorMath.maxMath.minMath.sinMath.sqrtMath.tan

在某些情況下,當 `-0` 作為引數之一存在時,可以從這些方法中獲得 `-0` 返回值。例如,`Math.min(-0, +0)` 計算結果為 `-0`。請參閱各個方法的文件。

~, <<, >>

每個運算子在內部使用 ToInt32 演算法。由於內部 32 位整數型別中只有一個表示形式為 0,因此 `-0` 在逆運算後不會經過往返。例如,`Object.is(~~(-0), -0)` 和 `Object.is(-0 << 2 >> 2, -0)` 都計算為 `false`。

當未考慮零的符號時,依賴 Object.is 可能很危險。當然,當意圖是區分 `-0` 和 `+0` 時,它恰好能按預期執行。

警告:Object.is() 和 NaN

Object.is 規範將所有 NaN 例項視為同一物件。但是,由於 型別化陣列 可用,因此我們可以擁有 `NaN` 的不同浮點表示形式,它們在所有上下文中並不完全相同。例如

js
const f2b = (x) => new Uint8Array(new Float64Array([x]).buffer);
const b2f = (x) => new Float64Array(x.buffer)[0];
// Get a byte representation of NaN
const n = f2b(NaN);
// Change the first bit, which is the sign bit and doesn't matter for NaN
n[0] = 1;
const nan2 = b2f(n);
console.log(nan2); // NaN
console.log(Object.is(nan2, NaN)); // true
console.log(f2b(NaN)); // Uint8Array(8) [0, 0, 0, 0, 0, 0, 248, 127]
console.log(f2b(nan2)); // Uint8Array(8) [1, 0, 0, 0, 0, 0, 248, 127]

另請參閱