In JavaScript, [NaN].includes(NaN) returns true but [NaN].indexOf(NaN) returns -1. Why do these two array methods disagree?
answer
- two search methods, two rules
- the older one predates the fix
- strict equality versus SameValueZero
- only NaN separates their answers
basics
~10 sThey use different comparisons. indexOf searches with strict equality, where NaN never matches itself, while includes searches with SameValueZero, which treats NaN as matching NaN. Both treat -0 and 0 as the same value.
solid answer
~40 s`Array.prototype.indexOf` compares candidates with strict equality, and `NaN === NaN` is `false`, so it can never report a position for `NaN`. `Array.prototype.includes`, added in ES2016, was specified with SameValueZero instead — the rule that matches `NaN` with `NaN` while still treating `-0` and `+0` as the same value. That is exactly the membership question people were trying to answer with `indexOf(x) !== -1` and failing on `NaN`. The same SameValueZero rule governs `Set` membership and `Map` keys, so a `Set` collapses duplicate `NaN` entries and a `Map` can be read back with a `NaN` key. `indexOf` was left alone because it is an ES5 method returning a position, and changing its comparison would silently alter existing code. Practical rule: if the data can contain `NaN`, ask with `includes`.
code
javascript · 8 linesconsole.log([NaN].includes(NaN)); // true — SameValueZero
console.log([NaN].indexOf(NaN)); // -1 — strict equality
console.log([-0].includes(0)); // true
console.log([0].indexOf(-0)); // 0
const s = new Set([NaN, NaN, 0, -0]);
console.log(s.size); // 2
console.log(Object.is([...s][1], -0)); // false — stored as +0go deeper
Recall the concrete outcome: includes can find NaN in an array and indexOf cannot. Say that the two methods use different comparison rules rather than guessing at coercion.
Name the algorithms — strict equality for indexOf and lastIndexOf, SameValueZero for includes — and state that NaN is the only value on which they disagree, since both treat -0 and 0 as the same.
Show where this bites in production: membership checks over parsed or computed numbers that can yield NaN, and keyed collections that normalise -0 on insert. Explain why the older method could not be changed compatibly.
Frame it as API-evolution policy: adding a correctly specified method beside an old one, rather than changing the old one's semantics, is the compatible move — and be ready to say what that costs in surface area and in team-wide confusion.
## Four comparisons, two of them here JavaScript has four sameness rules: loose equality, strict equality, SameValue (exposed as `Object.is`) and SameValueZero. The last two are almost the same rule; they differ only in how they treat the two zeros. - **Strict equality** — `NaN` matches nothing, `-0` matches `0`. - **SameValue** (`Object.is`) — `NaN` matches `NaN`, `-0` does *not* match `0`. - **SameValueZero** — `NaN` matches `NaN`, `-0` matches `0`. SameValueZero is the pragmatic middle: it fixes the one case where strict equality is useless for membership (`NaN`) without importing the one case where SameValue is pedantic (zero sign). Array search splits across the first and third of those rules. ## Which method uses which ```js [NaN].includes(NaN); // true — SameValueZero [NaN].indexOf(NaN); // -1 — strict equality [NaN].lastIndexOf(NaN); // -1 — strict equality [-0].includes(0); // true [0].indexOf(-0); // 0 — because -0 === 0 ``` `indexOf` and `lastIndexOf` compare with strict equality. `includes` compares with SameValueZero. Note that the zero behaviour is identical either way — `-0 === 0` is true and SameValueZero also matches them — so `NaN` is the *only* value on which the two methods can disagree. That single-value answer is what an interviewer is listening for. ## Why the split exists `indexOf` shipped in ES5 (2009) and answers "at what position?". People then used `arr.indexOf(x) !== -1` as a membership test, which works for everything except `NaN` — a real bug when a numeric pipeline can produce `NaN` from a failed parse or a division. When ES2016 added `includes`, it was a brand-new method that answers "is it in there?" and could pick its own comparison, so it was specified with SameValueZero to close that hole. Changing `indexOf` was never on the table: it is used everywhere, its return value is a position, and quietly making it match `NaN` would change the behaviour of shipped code. Adding a method with the right semantics was the compatible move. This is why the two coexist with different rules rather than one being a strict improvement. ## The same rule powers Set and Map SameValueZero is not an array quirk; it is the keying rule for the built-in keyed collections: ```js new Set([NaN, NaN]).size; // 1 — the duplicates collapse new Map([[NaN, 'x']]).get(NaN); // 'x' — a NaN key is retrievable new Set([0, -0]).size; // 1 — the zeros are one entry Object.is([...new Set([-0])][0], -0); // false — stored as +0 ``` The last line is the subtle one: the specification normalises the value, so inserting `-0` into a `Set` (or using it as a `Map` key) stores `+0`. Read the value back and its sign is gone. If the sign of zero is meaningful in your data, a `Set` or `Map` key is the wrong place to keep it. ## Strict equality shows up elsewhere too Anything built on strict equality inherits the `NaN` hole: ```js switch (value) { case NaN: /* unreachable, whatever value is */ break; } ``` `switch` selects its clause with strict equality, so a `case NaN:` label is dead code, and a `case -0:` label matches a plain `0`. If you must branch on `NaN`, test it explicitly before the `switch`. ## What to say in an interview The complete answer has four beats. First, name the two algorithms: strict equality for `indexOf`, SameValueZero for `includes`. Second, state precisely where they differ: only on `NaN`, because both treat `-0` and `0` as the same. Third, explain the history: `indexOf` is older, returns a position, and could not be changed compatibly, so the newer membership method got the better rule. Fourth, generalise: `Set` and `Map` use SameValueZero too, and `Object.is` sits one notch stricter by also separating the zeros. The practical takeaway is a habit, not trivia: when you are asking a membership question over data that could contain `NaN` — parsed user input, computed ratios, sensor readings — use `includes` or a `Set`, not `indexOf(x) !== -1`.
- Which comparison do Set membership and Map keys use?SameValueZero, the same rule as `Array.prototype.includes`. So a `Set` collapses duplicate `NaN` entries into one and a `Map` value stored under a `NaN` key is retrievable with `NaN`. The zeros are one entry too, and insertion normalises `-0` to `+0`, so reading the value back loses the sign — `Object.is([...new Set([-0])][0], -0)` is `false`.
- Why wasn't indexOf simply changed to match includes?`indexOf` shipped in ES5 and answers "at what position?"; code everywhere depends on its exact comparison, and silently matching `NaN` would change existing behaviour. `includes` arrived in ES2016 as a new method answering "is it present?", so it was free to be specified with SameValueZero without breaking anything.
- How does SameValueZero relate to the algorithm Object.is uses?They are the same rule with one exception: `Object.is` implements SameValue, which keeps `-0` and `+0` distinct, while SameValueZero treats them as equal. Both match `NaN` with `NaN`. So `Object.is(-0, 0)` is `false` but `[-0].includes(0)` and `new Set([0]).has(-0)` are both `true`.
saying these in an interview costs you the question
- Says includes and indexOf use the same comparison
- Claims [NaN].indexOf(NaN) returns 0
- Thinks Set membership uses Object.is semantics
- Believes a Set can hold both 0 and -0
- Says includes coerces its argument before searching