Array.includes Polyfill
Implement Array.prototype.includes with SameValueZero: NaN is found, +0 equals -0, and fromIndex can walk backwards from the end.
- javascript
- arrays
- polyfill
- interview
indexOf is the wrong engine for Array.includes. indexOf uses ===, so [NaN].indexOf(NaN) is -1. includes uses SameValueZero: NaN matches NaN, and +0 matches -0. Holes differ too. includes Gets every index, so a hole is undefined and includes(undefined) is true. indexOf uses HasProperty and skips holes, so indexOf(undefined) on a hole is -1. Copying indexOf and comparing to -1 fails both tests.
Write a standalone myIncludes(arrayLike, searchElement, fromIndex). Prototype assignment mutates a builtin.
Key takeaways
- Equality is SameValueZero:
NaNis found,+0equals-0.indexOfuses===and missesNaN. Do not implement includes asindexOf !== -1.- Holes are
undefinedviaGet.includes(undefined)istrueon a hole.- Negative
fromIndexislen + fromIndex, clamped to0.fromIndex >= lenisfalse.- Empty list is
falseimmediately.
SameValueZero, so NaN is found and +0 equals -0
SameValueZero is === except NaN equals NaN. It is Object.is except +0 and -0 are treated as equal. === already treats signed zeros as equal, which is why a one-line helper is “strict equal, or both are NaN.”
function sameValueZero(a, b) {
return a === b || (a !== a && b !== b);
}
function myIncludes(arrayLike, searchElement, fromIndex) {
if (arrayLike == null) {
throw new TypeError('myIncludes called on null or undefined');
}
const O = Object(arrayLike);
const len = O.length >>> 0;
if (len === 0) return false;
let n = fromIndex === undefined ? 0 : Number(fromIndex);
if (n !== n) n = 0;
if (n === Infinity) return false;
if (n === -Infinity) n = 0;
else n = Math.trunc(n);
let k = n >= 0 ? n : len + n;
if (k < 0) k = 0;
for (; k < len; k++) {
if (sameValueZero(searchElement, O[k])) {
return true;
}
}
return false;
}
myIncludes([1, NaN, 3], NaN); // true
myIncludes([+0], -0); // true
myIncludes([1, , 3], undefined); // true (hole)
myIncludes([1, 2, 3], 1, 1); // false
myIncludes([1, 2, 3], 3, -1); // true
myIncludes([1, 2, 3], 2, -1); // false
myIncludes([1, 2, 3], 1, 0.9); // true (truncates toward 0, starts at 0)
Object.is(a, b) || (a === 0 && b === 0) is the same helper. Object.is(+0, -0) is false; the === 0 clause puts signed zeros back together. Do not use ==. Do not use Object.is alone or you will miss -0 when searching for +0.
fromIndex
- If
lenis 0, returnfalse. - Let
nbeToIntegerOrInfinity(fromIndex). MissingfromIndexis0. - If
nis+Infinity, returnfalse. Ifnis-Infinity, start at0. - If
n >= 0, start atn. Else start atlen + n, not below0. - Walk
kup tolen - 1.Geteach slot. SameValueZero againstsearchElement.
A start at or past len never reads an element. A start of -100 on a short array is index 0, not an error. Fractional fromIndex truncates toward zero (Math.trunc, the whiteboard ToIntegerOrInfinity). Starting at 0.9 is index 0, not a lookup of the key "0.9".
includes vs indexOf vs Object.is
| Pair | === / indexOf |
Object.is |
includes (SameValueZero) |
|---|---|---|---|
NaN, NaN |
not found | equal | found |
+0, -0 |
found (=== is true) |
not equal | found |
hole, undefined |
indexOf skips the hole |
n/a | found |
present undefined |
found | equal | found |
const holes = [,];
holes.includes(undefined); // true
holes.indexOf(undefined); // -1
const nans = [NaN];
nans.includes(NaN); // true
nans.indexOf(NaN); // -1
find also Gets holes, but it runs a predicate and returns the element. includes is a value search with a fixed equality. some((x) => x === NaN) misses NaN; some(Number.isNaN) finds it. Prefer includes when the question is “is this value in the list?”
Footguns in an Array.includes polyfill
Objects compare by reference. includes({ a: 1 }) is false unless that exact object is in the array. Same as ===.
fromIndex is not a count of matches. ['a', 'a'].includes('a', 1) is still true because index 1 holds 'a'.
Strings are array-likes: myIncludes('abc', 'b') is true. Usually unwanted. Array.isArray is not part of the spec method; it is generic on purpose.
includes does not coerce the search value. myIncludes([1], '1') is false. fromIndex does go through ToIntegerOrInfinity, so true becomes 1 and '2' becomes 2. Those are different paths: the element is SameValueZero, the start index is a number conversion.
myIncludes([], NaN) is false without a comparison. fromIndex of Infinity is false without a comparison. Both are cheap exits, not “search then miss.”
Do not write arr.filter((x) => x === search)[0]. That uses === (misses NaN), skips holes, and allocates.
Standalone first. Array.prototype.myIncludes = function (search, from) { return myIncludes(this, search, from); } mutates a builtin.
Signed-zero and NaN background: Object.is vs === and IEEE-754.
Interview answer (out loud)
“includes walks from fromIndex with Get, so holes are undefined. Equality is SameValueZero: NaN matches NaN, and plus zero matches minus zero. I would not delegate to indexOf, because indexOf uses strict equality and skips holes. Negative fromIndex is length plus fromIndex, clamped to zero. If nothing matches, or the list is empty, or fromIndex is past the end, I return false.”
Further reading
- Array.prototype.includes: MDN
- Array.prototype.includes: ECMA-262
- Equality comparisons: MDN
- SameValueZero: ECMA-262
Related on this site
- Array.find polyfill
- Array.forEach polyfill
- Array find, some, every, includes
- Object.is vs ===
- Number precision and IEEE-754
- Polyfill questions playbook
- JavaScript interview learning path
Related guides
- Array.filter PolyfillWrite filter that skips holes, respects thisArg, and never writes empty slots for rejected items. Contrast with map’s hole-preserving result.
- Array.find PolyfillWrite Array.prototype.find: first callback hit wins, holes are read as undefined, nothing matching returns undefined, and findIndex is the index twin.
- Array.flat PolyfillImplement flat(depth) with Infinity, holes, and non-array array-likes. Recursion vs stack, and why concat(spread) blows up on huge inputs.
- Array.forEach PolyfillImplement Array.prototype.forEach: visit present elements, skip holes, ignore the callback return, and honor thisArg without using native forEach.
- Array.map PolyfillImplement Array.prototype.map without cheating: thisArg, sparse holes, callback args, and why indexOf-based loops fail on duplicates and NaN.