为什么 [NaN].includes(NaN) return 在 JavaScript 中为真?

Why does [NaN].includes(NaN) return true in JavaScript?

我很熟悉 NaN 在 JavaScript 中是“怪异的”,即 NaN === NaN 总是 returns false,如 [=20] 所述=].因此,不应进行 === 比较来检查 NaN,而应使用 isNaN(..)。

所以我很惊讶地发现

> [NaN].includes(NaN)
true

这似乎不一致。为什么会有这种行为?

它是如何工作的? includes 方法是否专门检查 isNaN?

正如您在阅读 include documentation, it does use the sameValueZero algorithm to work, so as its documentation 中看到的那样,它在比较 NaN 和我引用时给出了 True 值:

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.

.includes() 方法使用 SameValueZero 算法来检查两个值是否相等,它认为 NaN 值等于它自己。

SameValueZero算法与SameValue类似,唯一不同的是SameValueZero算法认为+0-0相等。

Object.is() 方法使用 SameValue 并且 returns 对 NaN 为真。

console.log(Object.is(NaN, NaN));

.includes() 方法的行为与 .indexOf() 方法略有不同; .indexOf() 方法使用严格相等比较来比较值,而严格相等比较不认为 NaN 等于自身。

console.log([NaN].indexOf(NaN));

有关不同等式检查算法的信息可在 MDN 中找到:

MDN - Equality comparisons and sameness

7.2.16 Strict Equality Comparison中,有如下注释:

NOTE

This algorithm differs from the SameValue Algorithm in its treatment of signed zeroes and NaNs.

这意味着 Array#includes 与严格比较不同的比较函数:

22.1.3.13 Array.prototype.includes

NOTE 3

The includes method intentionally differs from the similar indexOf method in two ways. First, it uses the SameValueZero algorithm, instead of Strict Equality Comparison, allowing it to detect NaN array elements. Second, it does not skip missing array elements, instead of treating them as undefined.

规格

这似乎是 Number::sameValueZero abstract operation 的一部分:

6.1.6.1.15 Number::sameValueZero ( x, y )

  1. If x is NaN and y is NaN, return true.

[...]

此操作必须是 Array#includes() 检查的一部分:

22.1.3.13 Array.prototype.includes ( searchElement [ , fromIndex ] )

[...]

  1. Repeat, while k < len
    a. Let elementK be the result of ? Get(O, ! ToString(k)).
    b. If SameValueZero(searchElement, elementK) is true, return true.
    c. Set k to k + 1.
  2. Return false.

[...]

其中 SameValueZero operation 将委托给第 2 步中的号码:

7.2.12 SameValueZero ( x, y )

[...]

  1. If Type(x) is different from Type(y), return false.
  2. If Type(x) is Number or BigInt, then
    a. Return ! Type(x)::sameValueZero(x, y).
  3. Return ! SameValueNonNumeric(x, y).

为了比较 Array#indexOf() will use Strict Equality Comparison 这就是它表现不同的原因:

const arr = [NaN];
console.log(arr.includes(NaN)); // true
console.log(arr.indexOf(NaN));  // -1


其他类似情况

使用SameValueZero进行比较的其他操作在集合和映射中:

const s = new Set();

s.add(NaN);
s.add(NaN);

console.log(s.size);     // 1
console.log(s.has(NaN)); // true

s.delete(NaN);

console.log(s.size);     // 0
console.log(s.has(NaN)); // false

const m = new Map();

m.set(NaN, "hello world");
m.set(NaN, "hello world");

console.log(m.size);     // 1
console.log(m.has(NaN)); // true

m.delete(NaN);

console.log(m.size);     // 0
console.log(m.has(NaN)); // false


历史

SameValueZero algorithm first appears in the ECMAScript 6 specifications 但它更冗长。它仍然具有相同的含义并且仍然具有显式:

7.2.10 SameValueZero(x, y)

[...]

  1. If Type(x) is Number, then a. If x is NaN and y is NaN, return true. [...]

ECMAScript 5.1 only has a SameValue algorithm 仍然将 NaN 视为等于 NaNSameValueZero 的唯一区别是 +0-0 的处理方式:SameValue returns false,而 SameValueZero returns true.

SameValue多用于内部对象操作,所以写JavaScript代码几乎无关紧要。 SameValue 的很多用途是在使用对象键并且没有数值时。

SameValue 操作直接暴露在 ECMAScript 6 中,因为那是 Object.is() 使用的:

console.log(Object.is(NaN, NaN)); // true
console.log(Object.is(+0, -0));   // false

稍微感兴趣的是 WeakMapWeakSet 也使用 SameValue 而不是 MapSet 用于比较的 SameValueZero .但是,WeakMapWeakSet 只允许对象作为唯一成员,因此尝试添加 NaN+0-0 或其他原语会导致错误。

MDN's document

Note: Technically speaking, includes() uses the sameValueZero algorithm to determine whether the given element is found.

const x = NaN, y = NaN;
console.log(x == y); // false                -> using ‘loose’ equality
console.log(x === y); // false               -> using ‘strict’ equality
console.log([x].indexOf(y)); // -1 (false)   -> using ‘strict’ equality
console.log(Object.is(x, y)); // true        -> using ‘Same-value’ equality
console.log([x].includes(y)); // true        -> using ‘Same-value-zero’ equality


更详细的解释:

  1. Same-value-zero equality similar to same-value equality,但是+0和-0被认为是相等的.
  2. 等值相等Object.is()方法提供:Object.is()===之间的唯一区别在于他们对有符号零和 NaN 的处理。


其他资源:

  • Which equals operator (== vs ===) should be used in JavaScript comparisons?
  • Are +0 and -0 the same?