How Does map() Work in JavaScript

November 5, 2025

javascript

Developers use map() every day, but few understand what JavaScript is actually doing when you call it. Let's build it from scratch to find out.

What is map() Doing?

At its core, map() is simple: it creates a new array by running your function on each element.

js
1.const numbers = [1, 2, 3, 4, 5];
2.const doubled = numbers.map(num => num * 2);
3.
4.console.log(doubled); // [2, 4, 6, 8, 10]
5.console.log(numbers); // [1, 2, 3, 4, 5] - original unchanged

But HOW does it do this? What's JavaScript actually executing?

Building map() From Scratch

Let's create map() step-by-step, starting simple and adding complexity as we go.

Step 1: The Simplest Possible Version

Let's start with the absolute simplest version that just doubles numbers:

js
1.function simpleMap(array) {
2. const result = [];
3. for (let i = 0; i < array.length; i++) {
4. result.push(array[i] * 2);
5. }
6. return result;
7.}
8.
9.const numbers = [1, 2, 3];
10.console.log(simpleMap(numbers)); // [2, 4, 6]

This works, but it's hardcoded to only double numbers. We need to make it flexible.

Step 2: Accept a Function as an Argument

Instead of hardcoding the transformation, let's accept a function:

js
1.function simpleMap(array, transformFunction) {
2. const result = [];
3. for (let i = 0; i < array.length; i++) {
4. const transformed = transformFunction(array[i]);
5. result.push(transformed);
6. }
7. return result;
8.}
9.
10.// Now we can transform however we want!
11.const numbers = [1, 2, 3];
12.console.log(simpleMap(numbers, n => n * 2)); // [2, 4, 6]
13.console.log(simpleMap(numbers, n => n * 3)); // [3, 6, 9]

This is the core concept: take a function, apply it to every element, build a new array with the results.

Step 3: Add It to Array.prototype

The real map() is a method on arrays. Let's add our function to Array.prototype:

js
1.Array.prototype.myMap = function(transformFunction) {
2. const result = [];
3. // 'this' refers to the array that called myMap()
4. for (let i = 0; i < this.length; i++) {
5. const transformed = transformFunction(this[i]);
6. result.push(transformed);
7. }
8. return result;
9.};
10.
11.const numbers = [1, 2, 3];
12.console.log(numbers.myMap(n => n * 2)); // [2, 4, 6]

Array.prototype usethis to reference the array within the function.

Step 4: Add Index and Array Parameters

The real map() passes three arguments to your callback: element, index, and the array itself:

js
1.Array.prototype.myMap = function(callback) {
2. const result = [];
3. for (let i = 0; i < this.length; i++) {
4. // Pass element, index, and the array itself
5. const transformed = callback(this[i], i, this);
6. result.push(transformed);
7. }
8. return result;
9.};
10.
11.const letters = ['a', 'b', 'c'];
12.const numbered = letters.myMap((letter, index) => {
13. return `${index + 1}. ${letter}`;
14.});
15.console.log(numbered); // ['1. a', '2. b', '3. c']

Step 5: Handle thisArg (Optional Second Parameter)

map() accepts an optional second parameter that sets what this refers to inside the callback:

js
1.Array.prototype.myMap = function(callback, thisArg) {
2. const result = [];
3. for (let i = 0; i < this.length; i++) {
4. // Call callback with the specified context
5. const transformed = callback.call(thisArg, this[i], i, this);
6. result.push(transformed);
7. }
8. return result;
9.};
10.
11.const multiplier = { factor: 10 };
12.const nums = [1, 2, 3];
13.const out = nums.myMap(function (n) {
14. return n * this.factor;
15.}, multiplier);
16.console.log(out); // [10, 20, 30]

That's it! We've built map() from scratch. The core mechanism is simple: create an empty array, loop through the original, call the callback on each element, and collect the results. Everything else: index parameters, thisArg, property checking, are just extras built on top of that core.

What's JavaScript Actually Doing?

Now that we understand the mechanism, let's trace through a concrete example to see exactly what JavaScript executes when you call map(). This will show you what's happening step-by-step in memory:

js
1.const numbers = [10, 20, 30];
2.const doubled = numbers.map(x => x * 2);
3.
4.// Step-by-step execution:
5.
6.// 1. JavaScript creates new empty array in memory: result = []
7.
8.// 2. Loop iteration 1 (i = 0):
9.// - Access numbers[0] which is 10
10.// - Call your function: x => x * 2 with x = 10
11.// - Function returns: 20
12.// - Push 20 to result: result = [20]
13.
14.// 3. Loop iteration 2 (i = 1):
15.// - Access numbers[1] which is 20
16.// - Call your function with x = 20
17.// - Function returns: 40
18.// - Push 40 to result: result = [20, 40]
19.
20.// 4. Loop iteration 3 (i = 2):
21.// - Access numbers[2] which is 30
22.// - Call your function with x = 30
23.// - Function returns: 60
24.// - Push 60 to result: result = [20, 40, 60]
25.
26.// 5. Loop ends, return result
27.// doubled = [20, 40, 60]

Length is Captured at Start

One interesting note, is that map() captures the array length BEFORE it starts looping. This means if you modify the array during mapping, those changes won't affect what gets visited:

js
1.const a = [1, 2, 3];
2.const out = a.map((x, i) => {
3. if (i === 0) a.push(4); // Add element during mapping
4. return x * 2;
5.});
6.
7.console.log(out); // [2, 4, 6] - the 4 wasn't visited
8.console.log(a); // [1, 2, 3, 4] - but it was added

Why does this happen? Because map() reads array.length once at the start (capturing it as 3 in this example), then iterates from 0 to 2. By the time the callback runs for index 0, it's too late to change what gets visited because the loop range was already determined.

Holes: Why map() Checks Property Existence

map() iterates over indices, but on each index it first asks “does this index exist as a property (own or inherited)?” If not, it doesn't call your function and leaves a hole at the same index in the output. If it exists, it calls your function and assigns to the same index in the new array.

What are "holes"?

A hole is an empty slot in an array. It's different from undefined:

js
1.// These look similar but are different
2.const hasUndefined = [1, undefined, 3]; // index 1 EXISTS, value is undefined
3.const hasHole = [1, , 3]; // index 1 DOESN'T EXIST (hole)
4.
5.console.log(1 in hasUndefined); // true - index 1 exists
6.console.log(1 in hasHole); // false - index 1 is a hole
7.
8.// You can also create holes with delete
9.const arr = [1, 2, 3];
10.delete arr[1]; // Creates a hole at index 1
11.console.log(arr); // [1, <1 empty item>, 3]

map() skips holes, meaning it won't call your callback for them. The hole is preserved in the output:

js
1.const sparse = [1, , 3];
2.const doubled = sparse.map(x => x * 2);
3.console.log(doubled); // [2, <1 empty item>, 6] - hole preserved

Here's what map() is doing internally to skip holes:

js
1.// This is what map() is doing internally:
2.Array.prototype.myMap = function(callback) {
3. const result = [];
4. for (let i = 0; i < this.length; i++) {
5. if (i in this) { // Check if property exists!
6. // Only call callback if the property exists
7. result[i] = callback(this[i], i, this);
8. }
9. }
10. result.length = this.length; // Preserve array length
11. return result;
12.};

The key is if (i in this). That in operator checks whether the property exists. For holes, it returns false, so the callback never runs.

Inherited Properties: An Unexpected Consequence

Now here's where it gets interesting. The in operator doesn't just check if a property exists on the array itself. It checks the ENTIRE prototype chain. This leads to a weird edge case:map() will visit indices that are inherited from Array.prototype.

To understand this, you need to know that arrays inherit from Array.prototype. Normally that just means they inherit methods like map and push. But you can also add numeric indices to the prototype:

js
1.// Weird edge case (don't do this in real code!)
2.Array.prototype[1] = "from prototype";
3.const arr = ["a", , "c"]; // hole at index 1
4.
5.const out = arr.map(v => v);
6.console.log(out[1]); // "from prototype" - callback WAS called!
7.
8.delete Array.prototype[1]; // Clean up

Why did the callback get called? Because 1 in arr returns true. The in operator checks the entire prototype chain, finds the property on Array.prototype[1], and considers it present. So map() calls your callback even though the array itself has a hole there.

Frequently Asked Questions

What does map() return?

A new Array. This is not the ES6 Map collection.

Can map() return something that is not a plain Array?

Yes, when you call it on an Array subclass. To build the result,map() picks a constructor based on the receiver (the spec calls this ArraySpeciesCreate). By default it uses the subclass constructor, unless the subclass overrides constructor[Symbol.species] to force a plain Array.

js
1.// Plain arrays -> Array
2.const arr = [1, 2, 3];
3.const doubled = arr.map(x => x * 2);
4.console.log(doubled instanceof Array); // true
5.
6.// Subclass default: result keeps the subclass
7.class Sub extends Array {}
8.const r1 = new Sub(1, 2, 3).map(x => x);
9.console.log(r1 instanceof Sub); // true
10.
11.// Subclass override: force plain Array results
12.class ForceArray extends Array {
13. static get [Symbol.species]() { return Array; }
14.}
15.const r2 = new ForceArray(1, 2, 3).map(x => x);
16.console.log(r2 instanceof ForceArray); // false
17.console.log(r2 instanceof Array); // true

Any performance tips when using map()?

Keep arrays packed (no holes) and types consistent. V8 runs packed, type-stable arrays faster than holey or mixed-type arrays.

js
1.// Fast: packed + consistent types
2.const fast = [1, 2, 3].map(x => x * 2);
3.
4.// Slower: holes degrade performance
5.const holey = [1, , 3].map(x => x * 2);
6.
7.// Slower: mixed types cause transitions
8.const mixed = [1, 2, 3].map(x => (x === 2 ? { val: x } : x));

Summary

At its core, map() is simple:

  1. Capture the array length at the start
  2. Create a new empty array
  3. Loop through each index (0 to length-1)
  4. Check if the property exists (using in)
  5. If it exists, call your function on that element
  6. Assign the result to the same index in the new array (holes preserved)
  7. Return the new array

Found this helpful? Follow for more tips and tutorials