Why does [NaN].includes(NaN) return true in JavaScript?
According to MDN's document say that
Note: Technically speaking,
includes()
uses thesameValueZero
algorithm to determine whether the given element is found.
const x = NaN, y = NaN;console.log(x == y); // false -> using ‘loose’ equalityconsole.log(x === y); // false -> using ‘strict’ equalityconsole.log([x].indexOf(y)); // -1 (false) -> using ‘strict’ equalityconsole.log(Object.is(x, y)); // true -> using ‘Same-value’ equalityconsole.log([x].includes(y)); // true -> using ‘Same-value-zero’ equality
More detailed explanation:
- Same-value-zero equality similar to same-value equality, but +0 and −0 are considered equal.
- 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.
Additional resources:
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
, 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:
Specs
This appears to be part of the Number::sameValueZero
abstract operation:
6.1.6.1.15 Number::sameValueZero ( x, y )
- 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 ] )
[...]
- 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.- Return false.
[...]
Where the SameValueZero
operation will delegate to the one for numbers at step 2:
7.2.12 SameValueZero ( x, y )
[...]
- If Type(x) is different from Type(y), return false.
- If Type(x) is Number or BigInt, then
a. Return ! Type(x)::sameValueZero(x, y).- 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)); // trueconsole.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); // 1console.log(s.has(NaN)); // trues.delete(NaN);console.log(s.size); // 0console.log(s.has(NaN)); // false
const m = new Map();m.set(NaN, "hello world");m.set(NaN, "hello world");console.log(m.size); // 1console.log(m.has(NaN)); // truem.delete(NaN);console.log(m.size); // 0console.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)
[...]
- If Type(x) is Number, thena. 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)); // trueconsole.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.