Array.prototype.map Polyfill
Implement Array.prototype.map from scratch. Master callback signatures, thisArg binding, and sparse-array hole preservation.
JavaScript · Arrays · Polyfills — Pratik Rai ·
Implement Array.prototype.filter from scratch. The key detail: push the original element, not the boolean result of the predicate.
Array.prototype.filter returns a new array containing only the elements whose callback returned a truthy value. It shares most of its machinery with map, but differs in one structural way that trips people up: the output indices do not line up with the input indices.
filter is where the difference between "returns the right values" and "matches the specification" becomes visible. The callback still receives the original index even though the result array is being packed densely, holes are still skipped, and the truthiness rules catch people who expected strict booleans.
This is the detail that separates filter from map:
JSfile.javascript1['a', 'b', 'c', 'd'].filter((value, index) => { 2 console.log(value, index); // 0, 1, 2, 3 — the source indices 3 return index % 2 === 0; 4}); 5// ['a', 'c'] — now at indices 0 and 1
The callback always sees where the element came from, never where it will end up. So the implementation needs two counters: one walking the source, one tracking the write position in the result.
JSfile.javascript1Array.prototype.myFilter = function (callback, thisArg) { 2 if (this === null || this === undefined) { 3 throw new TypeError('Array.prototype.myFilter called on null or undefined'); 4 } 5 if (typeof callback !== 'function') { 6 throw new TypeError(`${callback} is not a function`); 7 } 8 9 const object = Object(this); 10 const length = object.length >>> 0; 11 const result = []; 12 13 for (let i = 0; i < length; i++) { 14 if (i in object) { 15 const value = object[i]; 16 if (callback.call(thisArg, value, i, object)) { 17 result.push(value); 18 } 19 } 20 } 21 22 return result; 23};
Note that unlike map, push is correct here. The result is always dense — filter never produces holes, even when the input has them. Holes are simply skipped, never tested, and never carried through.
length is read once. Elements appended by the callback are not visited. Elements deleted before they are reached are not visited either, since the i in object check fails by then.
The return value is coerced to boolean. Any truthy value keeps the element. This is why [0, 1, 2, ''].filter(Boolean) is a common idiom for stripping falsy values — Boolean is just a function returning a truthy or falsy result.
It is generic. Like map, filter works on array-likes via call. Array.prototype.filter.call(document.querySelectorAll('div'), fn) was standard practice before Array.from existed.
thisArg binds the callback's this, and arrow functions ignore it.
Returning nothing is returning undefined. A callback with a missing return filters everything out, because undefined is falsy. With a braced arrow function this is easy to do by accident:
JSfile.javascript1items.filter(x => { x.active }); // always [] — no return 2items.filter(x => x.active); // correct
filter does not mutate. It always allocates a new array, even when nothing is removed. Chaining .filter().map().filter() over a large array allocates an intermediate at every step — usually irrelevant, occasionally the reason a hot loop is slow.
Filtering while iterating the same array is safe; filtering while mutating it is not. Deleting elements from the source inside the callback shifts subsequent indices, and the captured length no longer describes reality. If you need that, iterate backwards over a copy.
filter(Boolean) and TypeScript. The idiom works at runtime but does not narrow types on its own — you need a type predicate like (x): x is Foo => Boolean(x) for the compiler to drop null from the resulting element type.
"Implement filter using reduce." A neat one-liner, and a good check that you understand the accumulator:
JSfile.javascript1const myFilter = (arr, fn) => 2 arr.reduce((acc, value, i) => (fn(value, i, arr) ? [...acc, value] : acc), []);
Worth flagging in the same breath that spreading inside a reducer is quadratic — acc.push(value); return acc; is the version you would actually ship.
"How would you filter and transform in one pass?" flatMap with an empty array as the reject signal, or a plain reduce. This matters when the source is large enough that two passes cost real time.
length once and coerce with >>> 0.return empties the array.filter allocates a new array every time and never mutates the original.Goal: Implement myFilter correctly. Focus on pushing the original element, not the predicate result.
Continue learning with these related challenges
Implement Array.prototype.map from scratch. Master callback signatures, thisArg binding, and sparse-array hole preservation.
JavaScript · Arrays · Polyfills — Pratik Rai ·
Implement Array.prototype.flat from scratch with a configurable depth. Default depth is 1, not Infinity.
JavaScript · Arrays · Recursion — Pratik Rai ·
Implement Array.prototype.reduce from scratch, including the tricky no-initial-value path and the empty-array error case.
JavaScript · Arrays · Polyfills — Pratik Rai ·
Implement Array.prototype.map from scratch. Master callback signatures, thisArg binding, and sparse-array hole preservation.
JavaScript · Arrays · Polyfills
Pratik Rai ·
Implement Array.prototype.flat from scratch with a configurable depth. Default depth is 1, not Infinity.
JavaScript · Arrays · Recursion
Pratik Rai ·
Implement Array.prototype.reduce from scratch, including the tricky no-initial-value path and the empty-array error case.
JavaScript · Arrays · Polyfills
Pratik Rai ·