Array.filter Polyfill
Write filter that skips holes, respects thisArg, and never writes empty slots for rejected items. Contrast with map’s hole-preserving result.
- javascript
- arrays
- polyfill
- interview
map keeps length and keeps holes. filter does neither. A polyfill that does A = new Array(len) and writes A[k] = value when the predicate is true leaves empty slots for the items you dropped. Native filter([1, 2, 3], (x) => x !== 2) is [1, 3], dense, length 2. The other miss is calling the predicate on holes, so [1, , 3].filter(() => true) becomes [1, undefined, 3].
Skip holes. Keep values for which the callback is truthy. thisArg still applies. Standalone myFilter.
Key takeaways
- Same walk as
map:lengthplusk in O. Holes do not run the callback.- Output is dense.
pushthe kept values. Do not preserve indexes.- Predicate is
(value, index, array)withthisArg. Keep when the return value is truthy, not=== true.- Present
undefinedis a value. It can pass() => trueand appear in the result.filteris notmapwith a no-op on rejects. That would be sparse.
Keep the truthy callback results, drop the holes
function myFilter(arrayLike, callback, thisArg) {
if (arrayLike == null) {
throw new TypeError('myFilter called on null or undefined');
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function');
}
const O = Object(arrayLike);
const len = O.length >>> 0;
const res = [];
for (let k = 0; k < len; k++) {
if (k in O) {
const value = O[k];
if (callback.call(thisArg, value, k, O)) {
res.push(value);
}
}
}
return res;
}
if (callback.call(...)) uses JS truthiness. Native filter does the same: returning 1 keeps, returning 0 drops. filter(Boolean) therefore drops 0, '', false, null, undefined, NaN. That is often intended and sometimes a bug for numeric data. If you need “not nullish,” write that predicate.
push is correct here and wrong in map. The result length is “how many kept,” not O.length.
const sparse = [1, , 3, undefined];
myFilter(sparse, () => true);
// [1, 3, undefined] : hole gone, present undefined kept
myFilter(sparse, (x) => x);
// [1, 3] : undefined is falsy
myFilter([0, 1, 2], (x) => x);
// [1, 2] : 0 dropped
Indexes passed to the callback are source indexes. In [10, 20, 30].filter((_, i) => i !== 1) the callback still sees i === 2 for 30, not a compacted 1. You compact only the output.
thisArg:
const min = { n: 10 };
myFilter(
[5, 15, 25],
function (x) {
return x >= this.n;
},
min,
); // [15, 25]
An arrow as the callback ignores thisArg. That is language rules, not a polyfill bug. Use a function when you are testing thisArg.
Contrast with map’s holes
map |
filter |
|
|---|---|---|
| Hole in source | Skip callback, hole in output | Skip callback, nothing in output |
| Predicate / projector falsey | Still a slot (false, 0, undefined) |
Dropped |
| Result length | Source length |
Count of kept present elements |
| Build the result | Assign at k |
push |
If you allocate new Array(len) in filter and assign at k, you have written map with a predicate. [1, 2, 3] filtered by x > 1 would be [empty, 2, 3] (length 3). Native is [2, 3].
Do not implement filter by map then compacting holes. You would call the projector for the wrong set, and map’s callback is not a predicate.
Array-likes work the same as in map: { 0: 'a', 1: 'b', length: 2 } is a valid source. for...of would miss array-likes without an iterator and would visit holes as undefined. NodeLists in older browsers were array-like without being iterable; an indexed walk still filters them. Strings are array-like too (length and indexes), which is legal and usually unwanted.
Footguns in an Array.filter polyfill
filter(Boolean) on ids that include 0 is a production incident. Name it if they use that shorthand.
Sparse arrays from delete arr[i] or new Array(n) are where interviewers probe. delete makes a hole; arr[i] = undefined does not. After const a = [1, 2, 3]; delete a[1];, myFilter(a, () => true) is [1, 3]. After a[1] = undefined, the same predicate keeps undefined in the middle.
The callback may mutate later indexes. Length is captured up front, so growing the array does not visit the new tail. Shrinking it still walks the original len; missing indexes become holes and are skipped. That matches the spec’s “HasProperty then Get” loop, not a snapshot of values.
Do not use indexOf to find the next present element. Same duplicate / NaN lie as on the map polyfill.
Returning the source array when nothing was dropped is a mutation hazard if the caller then pushes. Always a new array, even if every element was kept. Native filter allocates every time; matching that is part of the contract, not an optimization you skip.
Assigning onto Array.prototype mutates a builtin. Prefer the standalone function unless they want arr.myFilter(...).
Interview answer (out loud)
“filter walks length with the in operator, skips holes, and pushes values whose callback is truthy. The result is dense; I never write empty slots for rejected items. The callback gets value, index, and the object, with thisArg via call. Present undefined can be kept; a hole cannot. That is the opposite of map, which keeps length and holes.”
Further reading
Related on this site
- Array.map polyfill
- Array.reduce polyfill
- Array map, filter, reduce
- Polyfill questions playbook
- Truthy and falsy values
- JavaScript interview learning path
Related guides
- 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.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.