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.
- 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
0or''.- Holes are visited.
Get, notHasProperty. Nok in Oguard.- No match returns
undefined. Do not return-1; that isfindIndex.filter(...)[0]skips holes, does not short-circuit, and is the wrong function.forEach/map/filterskip holes.find/findIndex/includesdo 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 on this site
- Array.forEach polyfill
- Array.includes polyfill
- Array.filter polyfill
- Array.map polyfill
- Array find, some, every, includes
- 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.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.includes PolyfillImplement Array.prototype.includes with SameValueZero: NaN is found, +0 equals -0, and fromIndex can walk backwards from the end.
- Array.map PolyfillImplement Array.prototype.map without cheating: thisArg, sparse holes, callback args, and why indexOf-based loops fail on duplicates and NaN.