Como o JavaScript é definido: do ECMAScript ao Array.prototype.map

Ilustração de Array.prototype.map transformando um array de números em valores duplicados.

Explore este artigo

Resumo rápido
  • O TC39 desenvolve a especificação ECMAScript; o ECMA-262 define o comportamento implementado pelos motores JavaScript.
  • Array.prototype.map verifica se cada propriedade existe, inclusive na cadeia de protótipos, antes de chamar o callback.
  • A especificação explica lacunas, argumentos do callback e uso genérico; o exemplo simplificado não é a implementação de um motor.

Abre o serviço escolhido com um prompt preparado. Se ele não conseguir ler o link, copie o artigo e cole no chat. Copie o prompt separadamente para usar outro chatbot.

Usamos JavaScript todos os dias com uma sintaxe como esta:

const numbers = [1, 2, 3];

const doubled = numbers.map(number => number * 2);

Sabemos o que map() faz, mas há uma pergunta mais interessante por trás disso:

Quem decidiu que map() deveria se comportar assim?

Por que ele pula posições vazias do array? Por que seu callback recebe o valor, o índice e o array? Por que Array.prototype.map pode até ser usado com objetos que não são arrays?

As respostas não são específicas do Chrome, do Firefox ou do Node.js. Elas vêm da definição formal da linguagem JavaScript.

Para entender isso, precisamos voltar um pouco.

Do JavaScript ao ECMAScript

O JavaScript foi criado por Brendan Eich na Netscape nos anos 1990 e apareceu pela primeira vez no Netscape Navigator. Depois, a Microsoft apresentou sua própria implementação relacionada, chamada JScript.

Isso criou um problema importante. Se empresas diferentes pudessem decidir de forma independente como a linguagem funcionava, o código escrito para um navegador poderia se comportar de outra forma em outro.

Era necessária uma definição comum da linguagem.

O trabalho de padronização começou na Ecma International em novembro de 1996, e a primeira edição do padrão ECMAScript foi adotada em junho de 1997.

A Ecma International é uma organização de padronização. Nela, diferentes comitês técnicos são responsáveis por tecnologias distintas. O comitê responsável pela linguagem padronizada do JavaScript é o TC39, ou Comitê Técnico 39.

Assim, podemos começar a estabelecer a relação:

Ecma International → TC39 → ECMAScript specification

O TC39 discute e desenvolve mudanças na linguagem. Novos recursos passam por um processo de propostas antes de integrar o padrão.

Mas onde entra o ECMA-262?

ECMA-262, ECMAScript e JavaScript

ECMA-262 é o número do padrão Ecma que define a linguagem ECMAScript.

Seu nome é:

ECMAScript Language Specification

Portanto, esses termos descrevem coisas diferentes:

TermoO que é
Ecma InternationalOrganização de padronização
TC39Comitê técnico responsável pelo ECMAScript
ECMA-262Documento do padrão que define o ECMAScript
ECMAScriptEspecificação da linguagem de programação definida pelo ECMA-262
JavaScriptLinguagem de programação baseada na especificação ECMAScript e que a implementa

O ECMA-262 é publicado periodicamente em novas edições.

Por exemplo, o padrão publicado atualmente é o ECMA-262, 17ª edição, que define o ECMAScript 2026.

TermoSignificado
ECMA-262Identificador do padrão
17ª ediçãoEdição do padrão ECMA-262
ECMAScript 2026Versão do ECMAScript definida por essa edição

A 17ª edição foi publicada em junho de 2026 e define o ECMAScript 2026.

O número 262, portanto, não é uma versão do JavaScript.

Da mesma forma, ECMAScript 2026 não significa “versão 2026” no sentido tradicional de versões de software. O ECMAScript segue um modelo de lançamento anual há vários anos.

Também existe uma especificação viva mantida pelo TC39. Depois da publicação do recorte anual, propostas concluídas e mudanças na especificação continuam sendo incorporadas ao documento vivo. Em outubro de 2026, esse documento já se chama ECMAScript 2027.

Isso nos dá uma visão mais ampla:

Diagrama horizontal da padronização e implementação do JavaScript: Ecma International → TC39 → ECMA-262 → linguagem ECMAScript → motores JavaScript → código. Cada etapa aparece em um retângulo conectado por setas sobre um fundo escuro.

Agora podemos voltar ao map().


De onde vem o map()?

Considere:

const numbers = [10, 20, 30];

numbers.map(number => number * 2);

Pode parecer que o próprio numbers contém uma função map.

Não contém.

Object.hasOwn(numbers, "map");
// false

Os arrays herdam esse método pela cadeia de protótipos:

numbers → Array.prototype → Object.prototype → null

E:

numbers.map === Array.prototype.map;
// true

Portanto, quando escrevemos:

numbers.map(callback);

O JavaScript encontra map em Array.prototype e o chama usando numbers como valor de this.

Conceitualmente:

Array.prototype.map.call(numbers, callback);

É aqui que a definição de Array.prototype.map no ECMAScript se torna relevante.

Lendo a especificação

A especificação descreve map() aproximadamente assim:

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.

A definição está na ECMAScript® 2026 Language Specification .

Isso não é código-fonte JavaScript.

Operações como ToObject, HasProperty, Get e Call são chamadas de operações abstratas. São ferramentas que a especificação usa para descrever formalmente como o JavaScript deve se comportar.

Vamos traduzir esse algoritmo aos poucos.

Primeiro, identificar o que está sendo mapeado

O algoritmo começa com:

Let O be ? ToObject(this value).

Em:

numbers.map(callback);

this é numbers.

Então, no nosso exemplo:

O = numbers

A especificação o chama deliberadamente de O, representando um objeto, em vez de algo como array.

Isso acontece porque map() é mais genérico do que parece. Ele também pode trabalhar com alguns objetos que não são arrays.

Em seguida:

Let len be ? LengthOfArrayLike(O).

Para:

const numbers = [10, 20, 30];

Temos:

len = 3

O algoritmo agora sabe que precisa considerar as posições 0, 1 e 2.

Depois, verifica se o callback pode ser chamado e cria o array que conterá o resultado.

Podemos aproximar esse início com:

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

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

const A = new Array(len);

A especificação real é mais precisa, mas esse é um modelo mental útil.

De um índice a uma propriedade

Agora começa a parte interessante.

O algoritmo inicializa:

k = 0

E repete enquanto:

k < len

Mas ele não acessa simplesmente:

O[k];

Antes, a especificação cria algo chamado Pk:

Let Pk be ! ToString(𝔽(k)).

Se:

k = 1

Então, conceitualmente:

k
↓
1

ToString(...)
↓
"1"

Pk = "1"

Por que converter o índice em uma string?

Porque um Array também é um Object.

Considere:

const arr = ["A", "B", "C"];

No nível da linguagem, podemos pensar nas propriedades indexadas assim:

"0" → "A"
"1" → "B"
"2" → "C"

É por isso que:

arr[1] === arr["1"];
// true

As chaves de propriedades comuns do JavaScript são strings ou símbolos. A especificação transforma a posição numérica usada pelo algoritmo na chave de propriedade usada para inspecionar o objeto.

Isso não significa que um motor JavaScript precise armazenar fisicamente "1" como string em algum lugar da memória.

A especificação define o comportamento. A implementação pode otimizá-lo internamente.

Essa propriedade realmente existe?

Depois de obter Pk, o algoritmo pergunta:

Let kPresent be ? HasProperty(O, Pk).

Uma aproximação útil é:

const kPresent = Pk in O;

Essa distinção importa porque arrays JavaScript podem conter lacunas.

Compare:

const a = [10, undefined, 30];

const b = [10, , 30];

Ambos produzem:

a[1]; // undefined
b[1]; // undefined

Mas estruturalmente são diferentes:

1 in a;
// true

1 in b;
// false

Em a, a propriedade existe e seu valor é undefined.

Em b, a propriedade não existe.

Por isso, map() não pode simplesmente recuperar o valor e verificar se ele é undefined.

Primeiro, ele pergunta:

Does property "1" exist?

Somente se a resposta for true ele chama o callback.

Isso explica este comportamento:

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

Saída:

10
30

O callback nunca é executado para a propriedade ausente.

HasProperty e a cadeia de protótipos

Há outro detalhe sutil.

HasProperty não inspeciona apenas as propriedades que pertencem diretamente ao array.

Ele também considera a cadeia de protótipos.

Isso o aproxima mais de:

"1" in arr

Do que de:

Object.hasOwn(arr, "1")

Por exemplo:

const arr = new Array(3);

Array.prototype[1] = 100;

O próprio array ainda não possui "1":

Object.hasOwn(arr, "1");
// false

Mas:

"1" in arr;
// true

Porque o JavaScript pode encontrá-lo aqui:

arr
 │
 │ "1"? no
 ▼
Array.prototype
 │
 │ "1"? yes
 └── 100

Portanto:

arr[1];
// 100

E como map() usa HasProperty, isso também o afeta:

arr.map(value => {
  console.log(value);
});

O callback recebe 100.

Este exemplo é propositalmente incomum, e modificar protótipos nativos assim deve ser evitado em geral. Mas ele mostra como map() interage de perto com o modelo de objetos do JavaScript.

Lendo e mapeando o valor

Depois de confirmar que a propriedade existe, a especificação executa:

Get(O, Pk)

Para:

const numbers = [10, 20, 30];

Quando:

Pk = "1"

Temos:

Get(numbers, "1")
→ 20

Esse valor se torna kValue.

A especificação então chama:

Call(callback, thisArg, « kValue, 𝔽(k), O »)

O que pode ser entendido aproximadamente como:

callback.call(
  thisArg,
  kValue,
  k,
  O
);

Isso também explica de onde vêm os argumentos conhecidos do callback:

numbers.map((value, index, array) => {
  // ...
});

Eles correspondem diretamente a:

kValue → value
k      → index
O      → array

Se nosso callback for:

value => value * 2

Então:

kValue = 20

callback(20, 1, numbers)
↓
40

A especificação chama esse resultado de:

mappedValue

Por fim, cria a propriedade correspondente no array de resultado:

CreateDataPropertyOrThrow(A, Pk, mappedValue)

Que podemos imaginar aproximadamente como:

A[Pk] = mappedValue;

Depois, k é incrementado e o processo se repete.

Juntando as peças

Uma versão simplificada do que acabamos de ler seria:

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;
}

Essa não é a implementação real usada pelo V8 ou por outro motor JavaScript, nem uma reimplementação completamente fiel de cada operação abstrata do ECMAScript.

Mas ela expõe a lógica da especificação.

Para:

const numbers = [10, 20];

numbers.map(value => value * 2);

Podemos acompanhar a primeira iteração:

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

Depois:

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

E, finalmente:

[20, 40]

O que parecia um simples laço se apoia em várias partes fundamentais do JavaScript: objetos, chaves de propriedades, busca em protótipos, existência de propriedades, chamadas de funções e criação de arrays.

Esse é o valor de ler o ECMA-262. Saímos de saber como usar JavaScript para entender por que ele se comporta dessa forma.