ESC

Type to search the knowledge base.

Array.find Polyfill

Write Array.prototype.find: first callback hit wins, holes are read as undefined, nothing matching returns undefined, and findIndex is the index twin.

beginner5 min read
  • javascript
  • arrays
  • polyfill
  • interview

filter(predicate)[0] is not Array.find. filter uses HasProperty and skips holes. find uses Get and visits holes as undefined. On [, 'x'], find((v) => v === undefined) returns undefined at index 0 without ever looking at 'x'. The same predicate through filter skips the hole and returns [], so [0] is undefined for a different reason. Implementing find as “filter and take the first” fails that test, keeps walking after a match, and allocates a whole array.

Write a standalone myFind. myFindIndex is the same walk with a different return. Prototype assignment mutates a builtin.

Key takeaways

  • Walk indexes in order. First truthy callback return wins. The element itself may be 0 or ''.
  • Holes are visited. Get, not HasProperty. No k in O guard.
  • No match returns undefined. Do not return -1; that is findIndex.
  • filter(...)[0] skips holes, does not short-circuit, and is the wrong function.
  • forEach / map / filter skip holes. find / findIndex / includes do not.

First match, holes read as undefined, undefined when nothing matches

Spec shape: ToObject, length, then for each k, kValue = Get(O, Pk), call the predicate, if ToBoolean of that result is true, return kValue. After the last index, return undefined. There is no presence check.

function myFind(arrayLike, predicate, thisArg) {
  if (arrayLike == null) {
    throw new TypeError('myFind called on null or undefined');
  }
  if (typeof predicate !== 'function') {
    throw new TypeError('predicate must be a function');
  }

  const O = Object(arrayLike);
  const len = O.length >>> 0;

  for (let k = 0; k < len; k++) {
    const value = O[k]; // Get, including holes as undefined
    if (predicate.call(thisArg, value, k, O)) {
      return value;
    }
  }
  return undefined;
}

function myFindIndex(arrayLike, predicate, thisArg) {
  if (arrayLike == null) {
    throw new TypeError('myFindIndex called on null or undefined');
  }
  if (typeof predicate !== 'function') {
    throw new TypeError('predicate must be a function');
  }

  const O = Object(arrayLike);
  const len = O.length >>> 0;

  for (let k = 0; k < len; k++) {
    const value = O[k];
    if (predicate.call(thisArg, value, k, O)) {
      return k;
    }
  }
  return -1;
}

myFind([, 'x'], (v) => v === undefined);      // undefined (hole at 0)
myFindIndex([, 'x'], (v) => v === undefined); // 0
myFind([1, 2, 3], (v) => v > 9);              // undefined
myFindIndex([1, 2, 3], (v) => v > 9);         // -1
myFind([0, 2], (v) => v === 0);               // 0 (element is falsy, predicate is not)

The last line is why you do not write if (value). find tests the predicate’s return, not the element. find((x) => x) on [0, 2] returns 2 because 0 is a falsy test result.

Hole families

Method Presence check Hole at index 0 in [, 'x']
forEach / map / filter / some / every HasProperty (k in O) callback not called
find / findIndex Get callback called with undefined
includes Get treated as undefined
indexOf HasProperty skipped
const sparse = [, 'x'];

// find: hole is a real visit
myFind(sparse, (v) => true); // undefined

// filter then [0]: hole skipped, first kept value is 'x'
// sparse.filter((v) => true)[0] === 'x'

const denseUndefined = [undefined, 'x'];
myFind(denseUndefined, (v) => v === undefined); // undefined (present slot)

Present undefined and a hole both Get as undefined, so find((v) => v === undefined) cannot tell them apart. forEach can: it skips the hole and visits the present slot. That contrast is the interview.

Length is captured up front, same as the other extras. A callback that pushes does not extend the search. A callback that deletes a later index still has that index visited if it stays below the captured length: Get returns undefined after a delete.

An empty array-like never calls the predicate. myFind([], () => true) is undefined. myFindIndex([], () => true) is -1. Same for length: 0 objects.

findLast and findLastIndex (ES2023) are the reverse walk with the same Get rule: a trailing hole is still undefined and can match. Mention them as extra credit. The interview polyfill is the forward pair.

Footguns in an Array.find polyfill

Do not return -1 on failure. Callers write if (found) and -1 is truthy in the worst way; the spec return is undefined. findIndex is the API that uses -1.

Do not stop at the first truthy element. myFind([0, 1], Boolean) is 1. myFind([0, 1], (n) => n === 0) is 0.

thisArg still applies. Borrow a method as the predicate and you need call. Arrows ignore it.

const min = { n: 10 };
myFind([4, 15, 8], function (value) {
  return value >= this.n;
}, min); // 15

The return is that live Get, the same reference as O[k]. find does not copy the element. If you then mutate the object, you are mutating the item still in the array.

Objects match by reference. find((x) => x === { id: 1 }) is always a miss. Compare a field, or use a Map.

find short-circuits. filter does not. On a million-row list where the hit is first, that is the reason to reach for find. The hole rule is the reason not to fake it with filter.

Standalone first. Array.prototype.myFind = function (cb, thisArg) { return myFind(this, cb, thisArg); } mutates a builtin.

App-level when-to-use sits on find, some, every, includes. some and every skip holes; this polyfill must not.

Interview answer (out loud)

“find captures length and Get at every index, including holes, which show up as undefined. I call the predicate with thisArg, value, index, and the object, and I return the first value whose predicate is truthy. If nothing matches I return undefined, not -1. findIndex is the same loop returning the index or -1. I would not implement this as filter()[0], because filter skips holes and keeps iterating.”

Further reading

Related guides