How JavaScript Is Defined: From ECMAScript to Array.prototype.map

How JavaScript Is Defined: From ECMAScript to Array.prototype.map

# javascript# programming# beginners# learning
How JavaScript Is Defined: From ECMAScript to Array.prototype.mapRafael

We use JavaScript every day through syntax such as: const numbers = [1, 2, 3]; const doubled =...

We use JavaScript every day through syntax such as:

const numbers = [1, 2, 3];

const doubled = numbers.map(number => number * 2);
Enter fullscreen mode Exit fullscreen mode

We know what map() does, but there is a more interesting question underneath:

Who decided that map() should behave this way?

Why does it skip empty array slots? Why does its callback receive the value, index, and array? Why can Array.prototype.map even be used with objects that are not arrays?

The answers are not specific to Chrome, Firefox, or Node.js. They come from the formal definition of the JavaScript language.

To understand that, we need to go back a little.

From JavaScript to ECMAScript

JavaScript was created by Brendan Eich at Netscape in the 1990s and first appeared in Netscape Navigator. Microsoft later introduced its own related implementation, called JScript.

This created an important problem. If different companies could independently decide how the language worked, code written for one browser could behave differently in another.

A common definition of the language was needed.

Standardization work started at Ecma International in November 1996, and the first edition of the ECMAScript standard was adopted in June 1997.

Ecma International is a standards organization. Inside it, different technical committees are responsible for different technologies. The committee responsible for JavaScript's standardized language is TC39, or Technical Committee 39.

So we can start building the relationship:

Ecma International → TC39 → ECMAScript specification
Enter fullscreen mode Exit fullscreen mode

TC39 discusses and develops changes to the language. New features go through a proposal process before becoming part of the standard.

But where does ECMA-262 fit into this?

ECMA-262, ECMAScript, and JavaScript

ECMA-262 is the number of the Ecma standard that defines the ECMAScript language.

Its name is:

ECMAScript Language Specification

So these terms describe different things:

Term What it is
Ecma International Standards organization
TC39 Technical committee responsible for ECMAScript
ECMA-262 Standard document that defines ECMAScript
ECMAScript Programming language specification defined by ECMA-262
JavaScript Programming language based on and implementing the ECMAScript specification

ECMA-262 is periodically published in new editions.

For example, the current published standard is ECMA-262, 17th Edition, which defines ECMAScript 2026.

Term Meaning
ECMA-262 The standard identifier
17th Edition The edition of the ECMA-262 standard
ECMAScript 2026 The ECMAScript version defined by that edition

The 17th edition was published in June 2026 and defines ECMAScript 2026.

The number 262 is therefore not a JavaScript version.

Likewise ECMAScript 2026 does not mean "version 2026" in the traditional software-version sense. ECMAScript has followed an annual release model for several years.

There is also a living specification maintained by TC39. After the yearly snapshot is published, completed proposals and specification changes continue to be incorporated into the living document. As of October 2026, that document is already titled ECMAScript 2027.

This gives us the larger picture:

A horizontal flow diagram showing how JavaScript is standardized and implemented: Ecma International → TC39 → ECMA-262 → ECMAScript Language → JavaScript Engines → Code. Each stage is displayed inside a rounded rectangular box connected by arrows on a dark background.

Now we can return to map().


Where does map() actually come from?

Consider:

const numbers = [10, 20, 30];

numbers.map(number => number * 2);
Enter fullscreen mode Exit fullscreen mode

It may look as if numbers itself contains a map function.

It does not.

Object.hasOwn(numbers, "map");
// false
Enter fullscreen mode Exit fullscreen mode

Arrays inherit that method through their prototype chain:

numbers → Array.prototype → Object.prototype → null
Enter fullscreen mode Exit fullscreen mode

And:

numbers.map === Array.prototype.map;
// true
Enter fullscreen mode Exit fullscreen mode

So when we write:

numbers.map(callback);
Enter fullscreen mode Exit fullscreen mode

JavaScript finds map on Array.prototype and calls it with numbers as its this value.

Conceptually:

Array.prototype.map.call(numbers, callback);
Enter fullscreen mode Exit fullscreen mode

This is where the ECMAScript definition of Array.prototype.map becomes relevant.

Reading the specification

The actual specification describes map() approximately like this:

1. Let O be ? ToObject(this value).
2. Let len be ? LengthOfArrayLike(O).
3. If IsCallable(callback) is false, throw a TypeError.
4. Let A be ? ArraySpeciesCreate(O, len).
5. Let k be 0.
6. Repeat while k < len:
   a. Let Pk be ! ToString(𝔽(k)).
   b. Let kPresent be ? HasProperty(O, Pk).
   c. If kPresent is true:
      i.   Let kValue be ? Get(O, Pk).
      ii.  Let mappedValue be ?
           Call(callback, thisArg, « kValue, 𝔽(k), O »).
      iii. Perform ?
           CreateDataPropertyOrThrow(A, Pk, mappedValue).
   d. Set k to k + 1.
7. Return A.
Enter fullscreen mode Exit fullscreen mode

It's defined in ECMAScript® 2026 Language Specification.

This is not JavaScript source code.

Operations such as ToObject, HasProperty, Get, and Call are called abstract operations. They are tools the specification uses to formally describe how JavaScript must behave.

Let's translate this algorithm gradually.

First, identify what is being mapped

The algorithm starts with:

Let O be ? ToObject(this value).
Enter fullscreen mode Exit fullscreen mode

For:

numbers.map(callback);
Enter fullscreen mode Exit fullscreen mode

this is numbers.

So, for our example:

O = numbers
Enter fullscreen mode Exit fullscreen mode

The specification deliberately calls it O, meaning an object, rather than something like array.

That is because map() is more generic than it first appears. It can work with some non-array objects too.

Next:

Let len be ? LengthOfArrayLike(O).
Enter fullscreen mode Exit fullscreen mode

For:

const numbers = [10, 20, 30];
Enter fullscreen mode Exit fullscreen mode

we get:

len = 3
Enter fullscreen mode Exit fullscreen mode

The algorithm now knows that it needs to consider positions 0, 1, and 2.

It then verifies that the callback is actually callable and creates the array that will contain the result.

We can approximate the beginning with:

const O = Object(this);
const len = O.length;

if (typeof callback !== "function") {
  throw new TypeError();
}

const A = new Array(len);
Enter fullscreen mode Exit fullscreen mode

The real specification is more precise than this, but this is a useful mental model.

From an index to a property

Now the interesting part begins.

The algorithm initializes:

k = 0
Enter fullscreen mode Exit fullscreen mode

and repeats until:

k < len
Enter fullscreen mode Exit fullscreen mode

But it does not simply access:

O[k];
Enter fullscreen mode Exit fullscreen mode

The specification first creates something called Pk:

Let Pk be ! ToString(𝔽(k)).
Enter fullscreen mode Exit fullscreen mode

If:

k = 1
Enter fullscreen mode Exit fullscreen mode

then conceptually:

k
↓
1

ToString(...)
↓
"1"

Pk = "1"
Enter fullscreen mode Exit fullscreen mode

Why convert the index to a string?

Because an Array is also an Object.

Consider:

const arr = ["A", "B", "C"];
Enter fullscreen mode Exit fullscreen mode

At the language level, we can think about its indexed properties like this:

"0" → "A"
"1" → "B"
"2" → "C"
Enter fullscreen mode Exit fullscreen mode

This is why:

arr[1] === arr["1"];
// true
Enter fullscreen mode Exit fullscreen mode

Normal JavaScript property keys are strings or symbols. The specification therefore turns the numeric position used by the algorithm into the property key that will be used to inspect the object.

This does not mean that a JavaScript engine must physically store "1" as a string somewhere in memory.

The specification defines behavior. The implementation is free to optimize that behavior internally.

Does that property actually exist?

After obtaining Pk, the algorithm asks:

Let kPresent be ? HasProperty(O, Pk).
Enter fullscreen mode Exit fullscreen mode

A useful approximation is:

const kPresent = Pk in O;
Enter fullscreen mode Exit fullscreen mode

This distinction is important because JavaScript arrays can contain holes.

Compare:

const a = [10, undefined, 30];

const b = [10, , 30];
Enter fullscreen mode Exit fullscreen mode

Both produce:

a[1]; // undefined
b[1]; // undefined
Enter fullscreen mode Exit fullscreen mode

But structurally they are different:

1 in a;
// true

1 in b;
// false
Enter fullscreen mode Exit fullscreen mode

In a, the property exists and its value is undefined.

In b, the property does not exist.

This is why map() cannot simply retrieve the value and check whether it is undefined.

It first asks:

Does property "1" exist?
Enter fullscreen mode Exit fullscreen mode

Only if the answer is true does it call the callback.

That explains this behavior:

[10, , 30].map(value => {
  console.log(value);
});
Enter fullscreen mode Exit fullscreen mode

Output:

10
30
Enter fullscreen mode Exit fullscreen mode

The callback is never executed for the missing property.

HasProperty and the prototype chain

There is another subtle detail.

HasProperty does not only inspect properties directly owned by the array.

It also considers the prototype chain.

That makes it closer to:

"1" in arr
Enter fullscreen mode Exit fullscreen mode

than:

Object.hasOwn(arr, "1")
Enter fullscreen mode Exit fullscreen mode

For example:

const arr = new Array(3);

Array.prototype[1] = 100;
Enter fullscreen mode Exit fullscreen mode

The array itself still does not own "1":

Object.hasOwn(arr, "1");
// false
Enter fullscreen mode Exit fullscreen mode

But:

"1" in arr;
// true
Enter fullscreen mode Exit fullscreen mode

because JavaScript can find it here:

arr
 │
 │ "1"? no
 ▼
Array.prototype
 │
 │ "1"? yes
 └── 100
Enter fullscreen mode Exit fullscreen mode

Therefore:

arr[1];
// 100
Enter fullscreen mode Exit fullscreen mode

And because map() uses HasProperty, this also affects it:

arr.map(value => {
  console.log(value);
});
Enter fullscreen mode Exit fullscreen mode

The callback receives 100.

This is an intentionally unusual example, and modifying native prototypes like this should generally be avoided. But it exposes how closely map() interacts with JavaScript's object model.

Reading and mapping the value

Once the property is known to exist, the specification performs:

Get(O, Pk)
Enter fullscreen mode Exit fullscreen mode

For:

const numbers = [10, 20, 30];
Enter fullscreen mode Exit fullscreen mode

when:

Pk = "1"
Enter fullscreen mode Exit fullscreen mode

we get:

Get(numbers, "1")
→ 20
Enter fullscreen mode Exit fullscreen mode

That becomes kValue.

The specification then calls:

Call(callback, thisArg, « kValue, 𝔽(k), O »)
Enter fullscreen mode Exit fullscreen mode

Which can be understood approximately as:

callback.call(
  thisArg,
  kValue,
  k,
  O
);
Enter fullscreen mode Exit fullscreen mode

This also explains where the familiar callback arguments come from:

numbers.map((value, index, array) => {
  // ...
});
Enter fullscreen mode Exit fullscreen mode

They directly correspond to:

kValue → value
k      → index
O      → array
Enter fullscreen mode Exit fullscreen mode

If our callback is:

value => value * 2
Enter fullscreen mode Exit fullscreen mode

then:

kValue = 20

callback(20, 1, numbers)
↓
40
Enter fullscreen mode Exit fullscreen mode

The specification calls that result:

mappedValue
Enter fullscreen mode Exit fullscreen mode

Finally, it creates the corresponding property in the result array:

CreateDataPropertyOrThrow(A, Pk, mappedValue)
Enter fullscreen mode Exit fullscreen mode

which we can roughly imagine as:

A[Pk] = mappedValue;
Enter fullscreen mode Exit fullscreen mode

Then k is incremented, and the process repeats.

Putting the pieces together

A simplified version of everything we have just read would look like this:

function simplifiedMap(callback, thisArg) {
  const O = Object(this);
  const len = O.length;

  if (typeof callback !== "function") {
    throw new TypeError();
  }

  const A = new Array(len);

  for (let k = 0; k < len; k++) {
    const Pk = String(k);

    if (Pk in O) {
      const kValue = O[Pk];

      const mappedValue = callback.call(
        thisArg,
        kValue,
        k,
        O
      );

      A[Pk] = mappedValue;
    }
  }

  return A;
}
Enter fullscreen mode Exit fullscreen mode

This is not the actual implementation used by V8 or another JavaScript engine, nor is it a completely faithful reimplementation of every ECMAScript abstract operation.

But it exposes the logic behind the specification.

For:

const numbers = [10, 20];

numbers.map(value => value * 2);
Enter fullscreen mode Exit fullscreen mode

we can now follow the first iteration:

k = 0
  ↓
Pk = "0"
  ↓
HasProperty(numbers, "0")
  ↓
 true
  ↓
Get(numbers, "0")
  ↓
 10
  ↓
callback(10, 0, numbers)
  ↓
 20
  ↓
result["0"] = 20
Enter fullscreen mode Exit fullscreen mode

Then:

k = 1
  ↓
Pk = "1"
  ↓
HasProperty(numbers, "1")
  ↓
 true
  ↓
Get(numbers, "1")
  ↓
 20
  ↓
callback(20, 1, numbers)
  ↓
 40
  ↓
result["1"] = 40
Enter fullscreen mode Exit fullscreen mode

And finally:

[20, 40]
Enter fullscreen mode Exit fullscreen mode

What initially looked like a simple loop is actually built on top of several fundamental parts of JavaScript: objects, property keys, prototype lookup, property existence, function calls, and array creation.

That is the value of reading ECMA-262. It moves us from knowing how to use JavaScript to understanding why JavaScript behaves the way it does.