ESC

Type to search the knowledge base.

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.

beginner5 min read
  • 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: length plus k in O. Holes do not run the callback.
  • Output is dense. push the kept values. Do not preserve indexes.
  • Predicate is (value, index, array) with thisArg. Keep when the return value is truthy, not === true.
  • Present undefined is a value. It can pass () => true and appear in the result.
  • filter is not map with 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 guides