Array.map Polyfill
Implement Array.prototype.map without cheating: thisArg, sparse holes, callback args, and why indexOf-based loops fail on duplicates and NaN.
- javascript
- arrays
- polyfill
- interview
A map that forEachs and pushes looks right on [1, 2, 3] and fails on [1, , 3]. Native Array.map skips the hole (no callback call) and keeps a hole in the output. push makes a dense [2, 6] and you just changed the length. The other whiteboard trap is walking the array with indexOf to “see if this index is real.” indexOf is the wrong tool: duplicates collapse to the first index, and NaN is never found.
Write a standalone myMap(arrayLike, callback, thisArg). Putting it on Array.prototype is interview convention and mutates a shared builtin.
Key takeaways
- Capture
length, loop0 .. length-1, call the callback only whenk in O.- Output is a new array of that length. Holes stay holes. Present
undefinedis a value.- Callback is
(value, index, array), invoked withthisArgviacall.indexOfcannot detect holes: duplicates andNaNmake it lie.- Do not implement
mapwithfilter,reduce, or the builtinmap.
Holes are not values, and indexOf is a trap
Spec shape, trimmed for a whiteboard: ToObject, length of the array-like, HasProperty per index, Call the callback, write a new array. Species (subclass instances) is extra credit; return a real Array.
function myMap(arrayLike, callback, thisArg) {
if (arrayLike == null) {
throw new TypeError('myMap 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 A = new Array(len);
for (let k = 0; k < len; k++) {
if (k in O) {
const mapped = callback.call(thisArg, O[k], k, O);
A[k] = mapped;
}
}
return A;
}
k in O is HasProperty. It is true for an own index and for a rare inherited index. It is false for a hole. Object.hasOwn(O, k) is stricter (own only). Prefer in if you are claiming spec; mention hasOwn if you want to ignore the prototype.
new Array(len) starts as holes. You only A[k] = when the source had that property. That is how holes survive. A.push(mapped) would pack them.
>>> 0 is the old ToUint32 trick. Current spec uses ToLength. For interview arrays it matches. Strings and { 0: 'a', 1: 'b', length: 2 } work because you used Object and length, not Array.isArray.
Why indexOf-based loops fail
This comes up when someone tries to skip holes without in:
// broken hole check
if (array.indexOf(array[k]) === k) {
// ...
}
| Input | What indexOf does |
What map needs |
|---|---|---|
[2, 2] at k = 1 |
indexOf(2) === 0, skip the second 2 |
Map both |
[NaN] |
indexOf(NaN) is -1 (NaN !== NaN) |
Map the NaN |
[1, , 1] at the hole |
array[1] is undefined; indexOf(undefined) may hit a real undefined elsewhere |
Skip the hole, keep the slot |
| Duplicate objects | First reference wins | Each index is independent |
indexOf searches by SameValueZero-ish equality from the left. It is a search API. map is an index API. Never combine them to walk every slot.
for...of and spread also lie about holes: they yield undefined and you will call the callback for a slot that native map skips. for...in picks up non-index keys and inherited enumerables. Indexed for plus in is the method.
Callback args and thisArg
const ctx = { factor: 10 };
const source = [1, , 3];
myMap(
source,
function (value, index, arr) {
return value * this.factor + index;
},
ctx,
);
// [10, empty, 32]
If you write callback(O[k], k, O) without call, methods that read this break, and arrows (which ignore thisArg) still work, so a test suite that only passes arrows will green-light a wrong polyfill.
Do not use fn(item) only. Interviewers look for the three arguments. The third is the object you mapped, not a copy.
Footguns in an Array.map polyfill
Present undefined is not a hole: [undefined].map(fn) calls fn. (1 in [1,]) is false; (0 in [undefined]) is true. Check the property, not === undefined.
Mutating the source during map is defined in terms of the original length and live Gets. Changing length downward still stops at the captured len in some engines’ mental model; the spec captured len up front. Capture len once.
Returning callback(...) into push drops holes. Returning a filtered list drops indexes. map is 1:1 on length, not on present elements. filter is the dense one.
Assigning Array.prototype.myMap = function (cb, thisArg) { return myMap(this, cb, thisArg); } is fine to mention. It is still a mutation.
This page is the spec polyfill. For when to use map versus a loop in app code, use array map, filter, reduce.
Interview answer (out loud)
“I ToObject the receiver, capture length, and for each index I use the in operator. If the slot is present I call the callback with thisArg, value, index, and the object, and I write the result at the same index of a new Array(length). Holes stay holes. I would not push, and I would not use indexOf to decide presence, because duplicates and NaN make indexOf skip real elements.”
Further reading
Related on this site
- Array.filter polyfill
- Array.reduce polyfill
- Array map, filter, reduce
- Polyfill questions playbook
- JavaScript interview learning path
- The event loop
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.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.