ESC

Type to search the knowledge base.

Array.includes Polyfill

Implement Array.prototype.includes with SameValueZero: NaN is found, +0 equals -0, and fromIndex can walk backwards from the end.

beginner5 min read
  • 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: NaN is found, +0 equals -0.
  • indexOf uses === and misses NaN. Do not implement includes as indexOf !== -1.
  • Holes are undefined via Get. includes(undefined) is true on a hole.
  • Negative fromIndex is len + fromIndex, clamped to 0. fromIndex >= len is false.
  • Empty list is false immediately.

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

  1. If len is 0, return false.
  2. Let n be ToIntegerOrInfinity(fromIndex). Missing fromIndex is 0.
  3. If n is +Infinity, return false. If n is -Infinity, start at 0.
  4. If n >= 0, start at n. Else start at len + n, not below 0.
  5. Walk k up to len - 1. Get each slot. SameValueZero against searchElement.

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

Related guides