Array.prototype.filter Polyfill
Implement Array.prototype.filter from scratch. The key detail: push the original element, not the boolean result of the predicate.
JavaScript · Arrays · Polyfills — Pratik Rai ·
Implement Array.prototype.map from scratch. Master callback signatures, thisArg binding, and sparse-array hole preservation.
Array.prototype.map builds a new array by running a callback over every element. Reimplementing it looks like a five-line for loop, and the five-line version is wrong in at least four ways. Working out which four is the point of the exercise.
map is the method people reach for without thinking, which makes it a good lens on how array methods actually behave. Almost everyone can write the loop. Far fewer can say what happens when the array has holes, when the callback deletes elements mid-iteration, or when map is called on something that is not an array at all.
Those are not trivia questions. They describe real behaviour you will eventually hit, usually while debugging something that makes no sense.
Here is what most people write first:
JSfile.javascript1Array.prototype.myMap = function (callback) { 2 const result = []; 3 for (let i = 0; i < this.length; i++) { 4 result.push(callback(this[i], i, this)); 5 } 6 return result; 7};
This produces the right answer for a dense array of values. It diverges from the native method the moment anything unusual happens.
Sparse arrays keep their holes. An array literal like [1, , 3] has a hole at index 1 — not undefined, an actual absence. Native map skips holes without calling the callback and preserves them in the output. The naive version calls the callback with undefined and fills the hole in. The fix is an in check:
JSfile.javascript1if (i in this) { 2 result[i] = callback.call(thisArg, this[i], i, object); 3}
Assigning by index rather than using push is what preserves the hole positions.
The length is captured once. The spec reads length before iterating. If the callback pushes new elements, those are never visited — otherwise map on a growing array would never terminate. Elements deleted or changed mid-iteration, however, are observed, because each index is read fresh.
The second argument sets this. map(callback, thisArg) binds this inside the callback. Forgetting this is the most common omission.
It works on array-likes. map is intentionally generic. Array.prototype.map.call({0: 'a', 1: 'b', length: 2}, fn) works. That is why the implementation begins by coercing with Object(this) and reading length as an unsigned integer rather than assuming a real array.
JSfile.javascript1Array.prototype.myMap = function (callback, thisArg) { 2 if (this === null || this === undefined) { 3 throw new TypeError('Array.prototype.myMap 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 = new Array(length); 12 13 for (let i = 0; i < length; i++) { 14 if (i in object) { 15 result[i] = callback.call(thisArg, object[i], i, object); 16 } 17 } 18 19 return result; 20};
>>> 0 is the idiomatic way to coerce length to a 32-bit unsigned integer, matching the spec's ToUint32. It turns -1, NaN and "3" into sane values, and it is the reason map on an object with length: -1 returns an empty array instead of looping forever.
new Array(length) pre-sizes the result so that untouched indices stay holes rather than becoming undefined.
map always returns the same length as its input. It cannot filter. [1,2,3].map(x => x > 1 ? x : undefined) gives you [undefined, 2, 3], not [2, 3]. Reaching for map when you meant filter or flatMap is a common source of stray undefined values.
The classic parseInt trap. ['1','2','3'].map(parseInt) returns [1, NaN, NaN]. map passes three arguments and parseInt takes two, so the index becomes the radix. This is the single most-asked map interview question, and it is really a question about how many arguments a callback receives.
this inside an arrow callback ignores thisArg. Arrow functions close over this lexically, so the second parameter has no effect on them. If you need thisArg, you need a regular function.
length once, up front, and coerce it with >>> 0.i in object to skip holes, and index assignment to preserve them.thisArg parameter, and remember arrows ignore it.map is generic — it works on anything with a length and indexed keys.(value, index, array), which is why map(parseInt) misbehaves.Goal: Implement myMap in under 15 minutes. Make sure all three callback arguments work and thisArg is respected.
Continue learning with these related challenges
Implement Array.prototype.filter from scratch. The key detail: push the original element, not the boolean result of the predicate.
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.filter from scratch. The key detail: push the original element, not the boolean result of the predicate.
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 ·