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

5

140

I'm familiar with NaN being "weird" in JavaScript, i.e., NaN === NaN always returns false, as described here. So one should not make === comparisons to check for NaN, but use isNaN(..) instead.

So I was surprised to discover that

> [NaN].includes(NaN)
true

This seems inconsistent. Why have this behavior?

How does it even work? Does the includes method specifically check isNaN?

Dennis answered 22/3, 2021 at 9:35 Comment(3)
you would like to read this post github.com/tc39/proposal-Array.prototype.includes/issues/29Loraineloralee
This seems inconsistent, Yes, it does seem strange. NaN is not equal to anything including NaN, apart from X. But I assume it's chosen here because if it didn't return true, having NaN in the array would then be pointless as it would never be found.Ridgway
@Ridgway it can be found with .some(). It's just a bit strange that this method was decided to actually work with NaN which is different to basically anything else. Aside from map and set. For those, it does make sense to treat NaN as the same value, otherwise the data structures become very useless.Militarist
F
148

According to MDN's document say that

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

More detailed explanation:

  1. Same-value-zero equality similar to same-value equality, but +0 and −0 are considered equal.
  2. Same-value equality is provided by the Object.is() method: The only difference between Object.is() and === is in their treatment of signed zeroes and NaNs.

enter image description here


Additional resources:

Frowsty answered 22/3, 2021 at 10:28 Comment(1)
I've just added additional resources & Re-arrange sample code to make it clearer as well as more understandable. As we may be familiar with lose & strict equality comparison, but I want to list out all of them to help us have the overall comparison between them. @IMSoPNishanishi
R
27

The .includes() method uses SameValueZero algorithm for checking the equality of two values and it considers the NaN value to be equal to itself.

The SameValueZero algorithm is similar to SameValue algorithm, but the only difference is that the SameValueZero algorithm considers +0 and -0 to be equal.

The Object.is() method uses SameValue and it returns true for NaN.

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

The behavior of .includes() method is slightly different from the .indexOf() method; the .indexOf() method uses strict equality comparison to compare values and strict equality comparison doesn't consider NaN to be equal to itself.

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

Information about different equality checking algorithms can be found at MDN:

MDN - Equality comparisons and sameness

Rocha answered 22/3, 2021 at 9:49 Comment(0)
M
18

Specs

This appears to be part of the Number::sameValueZero abstract operation:

6.1.6.1.15 Number::sameValueZero ( x, y )

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

[...]

This operation is required to be part of the Array#includes() check which does:

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.

[...]

Where the SameValueZero operation will delegate to the one for numbers at step 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).

For comparison Array#indexOf() will use Strict Equality Comparison which is why it behaves differently:

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

Other similar situations

Other operations that use SameValueZero for comparison are in sets and maps:

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

History

The SameValueZero algorithm first appears in the ECMAScript 6 specifications but it is more verbose. It still has the same meaning and still has an explicit:

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 which still treats NaN equal to NaN. The only difference with SameValueZero is how +0 and -0 are treated: SameValue returns false for them, while SameValueZero returns true.

SameValue is mostly used for internal object operation, so it is almost inconsequential for writing JavaScript code. A lot of the uses of SameValue are when working with object keys and there are no numeric values.

The SameValue operation is directly exposed in ECMAScript 6 as that is what Object.is() uses:

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

Of slight interest is that WeakMap and WeakSet also use SameValue rather than SameValueZero that Map and Set use for comparison. However, WeakMap and WeakSet only allow objects as unique members, so attempting to add a NaN or +0 or -0 or other primitives leads to an error.

Militarist answered 22/3, 2021 at 10:5 Comment(4)
Does "If x is NaN" mean exactly the same as "if isNaN(x)"? Or does it only refer to the single NaN value produced by NaN and not the entire set of NaN values?Gerard
@BenVoigt it's essentially the same as if (Number.isNaN(x)). Well, that would be the JavaScript equivalent. The algorithm steps are in pseudocode. The implementation is supposed to check if x is the literal value for NaN. There is only one of it in but different things can convert to it (e.g., Number("apple")). Edit: ref for NaN.Militarist
There definitely are multiple NaN bit patterns in IEEE-754, and I believe you can transfer IEEE-754 values into Javascript by using e.g. a typed buffer. So my question is, does SameValueZero treat two different NaN bit patterns as equal or unequal?Gerard
@BenVoigt ah, I guess that is where my knowledge of IEEE-754 stretches thin. But according to the spec NaN is a NaN even if it's not equal to itself. There should only be one conceptually. I suppose implementations are free to use as many NaN values as they want but if the algorithm says x is NaN it means be any valid NaN value.Militarist
S
8

In 7.2.16 Strict Equality Comparison, there is the following note:

NOTE

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

This means for Array#includes a different comparison function than for a strict comparison:

22.1.3.13 Array.prototype.includes under

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.

Sarracenia answered 22/3, 2021 at 9:56 Comment(0)
C
4

As you can see reading include documentation, it does use the sameValueZero algorithm to work, so as its documentation say, it gives a True value when comparing NaN and I quote:

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.

Creolacreole answered 22/3, 2021 at 9:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.