How to implement map function

Wednesday 27/07/2022

·5 min read
Share:

Implementing Array.prototype.map is a classic JavaScript interview question, and one of the best ways to internalize how higher-order array methods actually work. The one-line answer: walk the array, apply the callback to each element, push the result into a new array, return it. The spec-compliant version has more rules — handling sparse arrays, the thisArg parameter, and the three-argument callback signature — but the core idea is the same. Below: three versions from "interview answer" to "production polyfill."

Version 1: the minimal implementation

This is the version most candidates write, and it's correct for the 95% case:

Array.prototype.myMap = function (callback) {
    const result = []
    for (let i = 0; i < this.length; i++) {
        result.push(callback(this[i]))
    }
    return result
}

const arr = [1, 2, 3, 4]
console.log(arr.myMap(x => x * 2)) // [2, 4, 6, 8]

What's happening: this inside the method is the array map was called on. We iterate by index, apply the callback to each element, and collect results into a new array. Returning a new array (not mutating in place) is the contract — that's what makes map chainable.

Version 2: full callback signature

The real Array.prototype.map passes three arguments to the callback: the current element, its index, and the original array. It also accepts an optional thisArg for binding the callback's this. Many real callbacks rely on the index argument, so a complete implementation needs to pass it through:

Array.prototype.myMap = function (callback, thisArg) {
    const result = []
    for (let i = 0; i < this.length; i++) {
        result.push(callback.call(thisArg, this[i], i, this))
    }
    return result
}

const arr = ['a', 'b', 'c']
console.log(arr.myMap((el, idx) => `${idx}: ${el}`))
// ['0: a', '1: b', '2: c']

callback.call(thisArg, ...) binds the callback's this context — that's the optional second argument the built-in map accepts. If thisArg is undefined, call(undefined, ...) behaves like a normal invocation in strict mode.

Version 3: spec-compliant polyfill

The ECMAScript spec for Array.prototype.map handles a few subtle cases the minimal version misses:

  • Sparse arrays — slots that have never been assigned should not call the callback. [1, , 3] has length 3 but only two real elements.
  • Length captured once — if the callback mutates the array, the loop still uses the original length.
  • No callback throws a TypeError — calling [].map() without a function isn't legal.

Here's the closer-to-spec version:

Array.prototype.myMap = function (callback, thisArg) {
    if (typeof callback !== 'function') {
        throw new TypeError(callback + ' is not a function')
    }
    const len = this.length >>> 0  // coerce to unsigned 32-bit int
    const result = new Array(len)
    for (let i = 0; i < len; i++) {
        if (i in this) {
            result[i] = callback.call(thisArg, this[i], i, this)
        }
    }
    return result
}

The >>> 0 is the spec-mandated coercion to a 32-bit unsigned integer — it handles edge cases where someone sets arr.length = '3' or some other non-number. The i in this check is how we detect sparse slots without using hasOwnProperty.

Edge cases worth knowing

  • map does not iterate empty slots in sparse arrays, but it preserves them in the output. [1, , 3].map(x => x * 2) returns [2, <empty>, 6], not [2, NaN, 6].
  • map does not skip undefined values in dense arrays. [1, undefined, 3].map(x => x * 2) returns [2, NaN, 6]. Only "holes" (never-assigned slots) are skipped.
  • The callback can mutate the source array, but new elements added during iteration aren't visited because length is captured up front.
  • Returning from the callback is required. If your callback returns undefined (forgot the return), you get an array full of undefineds — a common bug when refactoring single-expression arrows into multi-line functions.

When to use map vs forEach vs for

| You need | Use | |---|---| | New array of transformed values | map | | Side effects without a return | forEach | | Early exit, complex flow | for / for...of | | Filtering AND transforming | flatMap or reduce |

map should always produce an array the same length as the input. If you find yourself returning undefined to "skip" elements, you want filter followed by map, or flatMap.

FAQ

What's the difference between map and forEach?

map returns a new array of the callback's return values. forEach returns undefined and is used purely for side effects. If you're not using the result of map, you should be using forEach.

Can map mutate the original array?

The callback can mutate the source, but the spec-compliant map reads length once before the loop and returns a new array. Mutating inside map is legal but bad style — pick forEach if you want side effects, or reduce if you want a single accumulator.

Why does my map implementation fail on sparse arrays?

The minimal version using for (let i = 0; i < this.length; i++) visits every index including unset slots, so the callback is called with undefined. Use if (i in this) to skip holes the way the built-in does.

Should I use Array.from instead of map?

Array.from(arr, callback) is equivalent to arr.map(callback) for arrays. The advantage of Array.from is it also works on array-like objects (NodeList, arguments) and iterables without needing to spread them first.

Share:
VA

Vadim Alakhverdov

Software developer writing about JavaScript, web development, and developer tools.

Related Posts