ESC

Type to search the knowledge base.

Array.map Polyfill

Implement Array.prototype.map without cheating: thisArg, sparse holes, callback args, and why indexOf-based loops fail on duplicates and NaN.

beginner5 min read
  • 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, loop 0 .. length-1, call the callback only when k in O.
  • Output is a new array of that length. Holes stay holes. Present undefined is a value.
  • Callback is (value, index, array), invoked with thisArg via call.
  • indexOf cannot detect holes: duplicates and NaN make it lie.
  • Do not implement map with filter, reduce, or the builtin map.

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 guides