平等比较和相同性

JavaScript 提供了三种不同的值比较操作:

¥JavaScript provides three different value-comparison operations:

  • === — 严格相等(三等号)
  • == — 松散相等(双重等于)
  • Object.is()

你选择哪种操作取决于你想要执行哪种比较。简要地:

¥Which operation you choose depends on what sort of comparison you are looking to perform. Briefly:

  • Double equals (==) 将在比较两个事物时执行类型转换,并将专门处理 NaN-0+0 以符合 IEEE 754(因此 NaN != NaN-0 == +0);
  • 三重等于(===)将进行与双等于相同的比较(包括对 NaN-0+0 的特殊处理),但不进行类型转换;如果类型不同,则返回 false
  • Object.is() 不进行类型转换,也不对 NaN-0+0 进行特殊处理(除了那些特殊数值之外,其行为与 === 相同)。

它们对应于 JavaScript 中四种相等算法中的三种:

¥They correspond to three of four equality algorithms in JavaScript:

请注意,它们之间的区别都与它们对原语的处理有关;它们都没有比较参数在结构上在概念上是否相似。对于任何具有相同结构但本身是不同对象的非原始对象 xy,所有上述形式都将计算为 false

¥Note that the distinction between these all have to do with their handling of primitives; none of them compares whether the parameters are conceptually similar in structure. For any non-primitive objects x and y which have the same structure but are distinct objects themselves, all of the above forms will evaluate to false.

使用 === 严格相等

¥Strict equality using ===

严格相等比较两个值是否相等。在比较之前,这两个值都不会隐式转换为其他值。如果值具有不同的类型,则这些值被视为不相等。如果值具有相同的类型、不是数字并且具有相同的值,则它们被视为相等。最后,如果两个值都是数字,则它们 '如果他们被认为是平等的' 都不是 NaN 并且是相同的值,或者如果一个是 +0,一个是 -0

¥Strict equality compares two values for equality. Neither value is implicitly converted to some other value before being compared. If the values have different types, the values are considered unequal. If the values have the same type, are not numbers, and have the same value, they're considered equal. Finally, if both values are numbers, they're considered equal if they're both not NaN and are the same value, or if one is +0 and one is -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 时。)

¥Strict equality is almost always the correct comparison operation to use. For all values except numbers, it uses the obvious semantics: a value is only equal to itself. For numbers it uses slightly different semantics to gloss over two different edge cases. The first is that floating point zero is either positively or negatively signed. This is useful in representing certain mathematical solutions, but as most situations don't care about the difference between +0 and -0, strict equality treats them as the same value. The second is that floating point includes the concept of a not-a-number value, NaN, to represent the solution to certain ill-defined mathematical problems: negative infinity added to positive infinity, for example. Strict equality treats NaN as unequal to every other value — including itself. (The only case in which (x !== x) is true is when x is NaN.)

除了 === 之外,数组索引查找方法也使用严格相等,包括 Array.prototype.indexOf()Array.prototype.lastIndexOf()TypedArray.prototype.indexOf()TypedArray.prototype.lastIndexOf()case 匹配。这意味着你不能使用 indexOf(NaN) 查找数组中 NaN 值的索引,也不能使用 NaN 作为 switch 语句中的 case 值并使其与任何内容匹配。

¥Besides ===, strict equality is also used by array index-finding methods including Array.prototype.indexOf(), Array.prototype.lastIndexOf(), TypedArray.prototype.indexOf(), TypedArray.prototype.lastIndexOf(), and case-matching. This means you cannot use indexOf(NaN) to find the index of a NaN value in an array, or use NaN as a case value in a switch statement and make it match anything.

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

使用 == 松散相等

¥Loose equality using ==

松散相等是对称的:对于 AB 的任何值,A == B 始终具有与 B == A 相同的语义(应用转换的顺序除外)。使用 == 执行松散相等的行为如下:

¥Loose equality is symmetric: A == B always has identical semantics to B == A for any values of A and B (except for the order of applied conversions). The behavior for performing loose equality using == is as follows:

  1. 如果操作数具有相同类型,则比较如下:
    • 对象仅当两个操作数引用同一对象时才返回 true
    • 字符串:仅当两个操作数具有相同顺序的相同字符时才返回 true
    • 数字:仅当两个操作数具有相同值时才返回 true+0-0 被视为相同的值。如果任一操作数为 NaN,则返回 false;所以 NaN 永远不等于 NaN
    • 布尔值:仅当操作数均为 true 或均为 false 时才返回 true
    • 大整数:仅当两个操作数具有相同值时才返回 true
    • 符合:仅当两个操作数引用相同符号时才返回 true
  2. 如果其中一个操作数是 nullundefined,则另一个操作数也必须是 nullundefined,才能返回 true。否则返回 false
  3. 如果其中一个操作数是对象,另一个是原语,则 将对象转换为原始对象
  4. 在此步骤中,两个操作数都转换为基元(String、Number、Boolean、Symbol 和 BigInt 之一)。其余的转换将根据具体情况进行。
    • 如果它们属于同一类型,请使用步骤 1 进行比较。
    • 如果其中一个操作数是符号而另一个不是,则返回 false
    • 如果其中一个操作数是布尔值,而另一个不是,则 将布尔值转换为数字true 转换为 1,false 转换为 0。然后再次松散地比较两个操作数。
    • 数字到字符串:将字符串转换为数字。转换失败会导致 NaN,这将保证相等为 false
    • 数字到 BigInt:通过它们的数值进行比较。如果数字为 ±Infinity 或 NaN,则返回 false
    • 字符串到 BigInt:使用与 BigInt() 构造函数相同的算法将字符串转换为 BigInt。如果转换失败,则返回 false

传统上,根据 ECMAScript,所有原语和对象都松散地不等于 undefinednull。但大多数浏览器允许非常狭窄的对象类别(具体来说,任何页面的 document.all 对象),在某些上下文中,就像它们模拟值 undefined 一样。松散平等就是这样的背景之一:当且仅当 A 是模拟 undefined 的对象时,null == Aundefined == A 才计算为 true。在所有其他情况下,对象永远不会松散地等于 undefinednull

¥Traditionally, and according to ECMAScript, all primitives and objects are loosely unequal to undefined and null. But most browsers permit a very narrow class of objects (specifically, the document.all object for any page), in some contexts, to act as if they emulate the value undefined. Loose equality is one such context: null == A and undefined == A evaluate to true if, and only if, A is an object that emulates undefined. In all other cases an object is never loosely equal to undefined or null.

在大多数情况下,不鼓励使用松散相等。使用严格相等的比较结果更容易预测,并且由于缺乏类型强制而可以更快地评估。

¥In most cases, using loose equality is discouraged. The result of a comparison using strict equality is easier to predict, and may evaluate more quickly due to the lack of type coercion.

以下示例演示了涉及数字基元 0、bigint 基元 0n、字符串基元 '0' 以及 toString() 值为 '0' 的对象的松散相等比较。

¥The following example demonstrates loose equality comparisons involving the number primitive 0, the bigint primitive 0n, the string primitive '0', and an object whose toString() value is '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

松散相等仅由 == 运算符使用。

¥Loose equality is only used by the == operator.

使用 Object.is() 实现同值相等

¥Same-value equality using Object.is()

同值相等确定两个值在所有上下文中是否功能相同。(此用例演示了 里氏替换原则 的一个实例。)当尝试改变不可变属性时会发生一个实例:

¥Same-value equality determines whether two values are functionally identical in all contexts. (This use case demonstrates an instance of the Liskov substitution principle.) One instance occurs when an attempt is made to mutate an immutable property:

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.defineProperty will throw an exception when attempting to change an immutable property, but it does nothing if no actual change is requested. If v is -0, no change has been requested, and no error will be thrown. Internally, when an immutable property is redefined, the newly-specified value is compared against the current value using same-value equality.

Object.is 方法提供同值相等。它几乎在语言中任何需要同等标识值的地方使用。

¥Same-value equality is provided by the Object.is method. It's used almost everywhere in the language where a value of equivalent identity is expected.

同值零相等

¥Same-value-zero equality

与同值相等类似,但 +0 和 -0 被视为相等。

¥Similar to same-value equality, but +0 and -0 are considered equal.

同值零相等不会作为 JavaScript API 公开,但可以使用自定义代码实现:

¥Same-value-zero equality is not exposed as a JavaScript API, but can be implemented with custom code:

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 方法使用它来比较键相等性。

¥Same-value-zero only differs from strict equality by treating NaN as equivalent, and only differs from same-value equality by treating -0 as equivalent to 0. This makes it usually have the most sensible behavior during searching, especially when working with NaN. It's used by Array.prototype.includes(), TypedArray.prototype.includes(), as well as Map and Set methods for comparing key equality.

比较平等方法

¥Comparing equality methods

人们经常通过说一个是另一个的 "enhanced" 版本来比较双等和三等。例如,双等号可以说是三等号的扩展版本,因为前者执行后者执行的所有操作,但对其操作数进行类型转换 - 例如,6 == "6"。或者,也可以说双等于是基线,三等于是增强版本,因为它要求两个操作数是同一类型,所以增加了额外的约束。

¥People often compare double equals and triple equals by saying one is an "enhanced" version of the other. For example, double equals could be said as an extended version of triple equals, because the former does everything that the latter does, but with type conversion on its operands — for example, 6 == "6". Alternatively, it can be claimed that double equals is the baseline, and triple equals is an enhanced version, because it requires the two operands to be the same type, so it adds an extra constraint.

然而,这种思维方式意味着相等比较形成一维 "spectrum",其中 "完全严格" 位于一端,"完全松动" 位于另一端。该模型不符合 Object.is,因为它既不是 "looser" 比双等号,也不是 "stricter" 比三等号,也不适合介于两者之间(即,既比双等号严格,又比三等号宽松)。从下面的相同性比较表中我们可以看出,这是由于 Object.is 处理 NaN 的方式造成的。请注意,如果 Object.is(NaN, NaN) 的计算结果为 false,我们可以说它符合松散/严格的范围,作为一种更严格的三等式形式,用于区分 -0+0。然而,NaN 处理意味着这是不正确的。不幸的是,必须根据 Object.is 的具体特性来考虑,而不是考虑其在相等运算符方面的松散或严格。

¥However, this way of thinking implies that the equality comparisons form a one-dimensional "spectrum" where "totally strict" lies on one end and "totally loose" lies on the other. This model falls short with Object.is, because it isn't "looser" than double equals or "stricter" than triple equals, nor does it fit somewhere in between (i.e., being both stricter than double equals, but looser than triple equals). We can see from the sameness comparisons table below that this is due to the way that Object.is handles NaN. Notice that if Object.is(NaN, NaN) evaluated to false, we could say that it fits on the loose/strict spectrum as an even stricter form of triple equals, one that distinguishes between -0 and +0. The NaN handling means this is untrue, however. Unfortunately, Object.is has to be thought of in terms of its specific characteristics, rather than its looseness or strictness with regard to the equality operators.

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() 与三重等于

¥When to use Object.is() versus triple equals

一般来说,当你的工作需要反映 Object.defineProperty 的某些特性时,唯一可能对 Object.is 对 0 的特殊行为感兴趣的时候是在追求某些元编程方案时,特别是在属性描述符方面。如果你的用例不需要这样做,建议避免使用 Object.is 并使用 ===。即使你的要求涉及将两个 NaN 值评估为 true 之间的比较,一般来说,对 NaN 检查进行特例处理(使用 ECMAScript 早期版本中可用的 isNaN 方法)比弄清楚周围的计算如何影响 你在比较中遇到的任何零的符号。

¥In general, the only time Object.is's special behavior towards zeros is likely to be of interest is in the pursuit of certain meta-programming schemes, especially regarding property descriptors, when it is desirable for your work to mirror some of the characteristics of Object.defineProperty. If your use case does not require this, it is suggested to avoid Object.is and use === instead. Even if your requirements involve having comparisons between two NaN values evaluate to true, generally it is easier to special-case the NaN checks (using the isNaN method available from previous versions of ECMAScript) than it is to work out how surrounding computations might affect the sign of any zeros you encounter in your comparison.

以下是内置方法和运算符的非详尽列表,这些方法和运算符可能会导致 -0+0 之间的区别在代码中显现出来:

¥Here's a non-exhaustive list of built-in methods and operators that might cause a distinction between -0 and +0 to manifest itself in your code:

-(一元否定)

考虑以下示例:

js
const 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 算法。由于 0 在内部 32 位整数类型中只有一种表示形式,因此 -0 在逆运算后将无法在往返中幸存。例如,Object.is(~~(-0), -0)Object.is(-0 << 2 >> 2, -0) 的计算结果都是 false

当不考虑零的符号时依赖 Object.is 可能是危险的。当然,当目的是区分 -0+0 时,它完全符合预期。

¥Relying on Object.is when the signedness of zeros is not taken into account can be hazardous. Of course, when the intent is to distinguish between -0 and +0, it does exactly what's desired.

警告:Object.is() 和 NaN

¥Caveat: Object.is() and NaN

Object.is 规范将 NaN 的所有实例视为同一对象。然而,由于 类型数组 可用,我们可以有 NaN 的不同浮点表示,但其在所有上下文中的行为并不相同。例如:

¥The Object.is specification treats all instances of NaN as the same object. However, since typed arrays are available, we can have distinct floating point representations of NaN which don't behave identically in all contexts. For example:

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]

也可以看看

¥See also