12k
All articles

Como funciona a cadeia de protótipos do JavaScript

Entenda a busca na cadeia de protótipos do JavaScript, a diferença entre prototype e [[Prototype]], como propriedades herdadas são ocultadas e como inspecionar os vínculos.

OpenReplay Team
OpenReplay Team
Como funciona a cadeia de protótipos do JavaScript

Quando você lê uma propriedade, o JavaScript verifica primeiro as propriedades próprias do objeto. Em seguida, segue o link interno [[Prototype]] até o próximo objeto e repete o processo até encontrar a propriedade ou chegar a null. Nesse último caso, a leitura retorna undefined.

A regra é simples, mas fica mais difícil de acompanhar quando você vê prototype, __proto__ e [[Prototype]] lado a lado em um painel do DevTools ou em uma pergunta de entrevista. O Firefox chega a rotular o link como <prototype> quando você expande um objeto no console.

Este artigo trata apenas de como a busca por uma propriedade percorre a cadeia: qual objeto responde a uma leitura, onde as escritas vão parar e quais APIs usar para inspecionar um protótipo. Para funções construtoras, classes e padrões de herança com Object.create, consulte A Beginner’s Guide to OOP in JavaScript e Inheritance in JavaScript Object-Oriented Programming.

Principais conclusões

  • A leitura de uma propriedade verifica as propriedades próprias e, depois, cada [[Prototype]] em sequência. Quando a cadeia termina em null, ela retorna undefined.
  • A propriedade prototype de uma função se torna o [[Prototype]] das instâncias criadas com new. Ela não é o protótipo da própria função.
  • __proto__ é um acessor obsoleto herdado de Object.prototype. Use Object.getPrototypeOf() e Object.setPrototypeOf() no lugar dele.
  • Atribuir um valor a uma propriedade herdada geralmente cria uma propriedade própria que sombreia a herdada e mantém o protótipo inalterado.
  • Object.create(null) gera um objeto sem cadeia, o que o torna um dicionário seguro para chaves de string arbitrárias.

Qual é a regra de busca na cadeia de protótipos do JavaScript?

A cadeia de protótipos do JavaScript é a sequência de objetos, ligados por meio de [[Prototype]], que a leitura de uma propriedade percorre até encontrar uma correspondência ou chegar a null. Todo objeto comum tem um slot interno [[Prototype]] que aponta para outro objeto ou para null. O algoritmo OrdinaryGet da especificação define como funciona uma leitura. Ele procura uma propriedade própria e, se não houver nenhuma, repete a busca no protótipo. Para a maioria dos objetos, a cadeia termina em Object.prototype, cujo protótipo é null.

const animal = { eats: true };
const rabbit = Object.create(animal);
rabbit.jumps = true;

console.log(rabbit.jumps);   // true      (own property)
console.log(rabbit.eats);    // true      (found on animal)
console.log(rabbit.toString === Object.prototype.toString); // true
console.log(rabbit.flies);   // undefined (not found anywhere)
console.log(Object.getPrototypeOf(Object.prototype)); // null
// rabbit -> animal -> Object.prototype -> null

Ler uma propriedade inexistente não gera erro. O que lança um TypeError é chamar rabbit.flies(), porque a chamada é feita sobre undefined. O guia Herança e a cadeia de protótipos da MDN aborda o mesmo modelo com mais profundidade.

Qual é a diferença entre o prototype de uma função e o [[Prototype]] de um objeto?

A propriedade prototype de uma função é o objeto que se torna o [[Prototype]] das instâncias criadas com new. Ela não é o protótipo da própria função, que é Function.prototype. Para ler o link real de qualquer objeto, use Object.getPrototypeOf().

class Greeter { hello() { return 'hi'; } }
const g = new Greeter();

console.log(Object.getPrototypeOf(g) === Greeter.prototype);        // true
console.log(Object.getPrototypeOf(Greeter) === Function.prototype); // true
console.log(Object.getPrototypeOf(Greeter) === Greeter.prototype);  // false

const arrow = () => {};
console.log(arrow.prototype);                   // undefined
console.log(Greeter.prototype.hello.prototype); // undefined

Arrow functions e métodos comuns de classe não têm nenhuma propriedade prototype, então ler .prototype neles retorna undefined.

NomeO que éOnde ficaComo ler
[[Prototype]]O link interno que a busca segueEm todo objeto (um slot interno)Object.getPrototypeOf(obj)
obj.__proto__Um getter/setter legado para esse linkObject.prototypeEvite; use getPrototypeOf
Fn.prototypeO objeto atribuído às instâncias criadas com new Fn()Funções construtíveis e classesFn.prototype

O que é proto?

__proto__ é uma propriedade acessora definida em Object.prototype. Ela não fica armazenada em cada objeto, então um objeto que não herda de Object.prototype não a possui. A MDN marca Object.prototype.__proto__ como Deprecated (obsoleto) e explica que ele faz parte do padrão apenas para que sites antigos continuem funcionando. A MDN recomenda usar Object.getPrototypeOf() e Object.setPrototypeOf() em vez dele.

const proto = { greet() { return 'hello'; } };
const obj = Object.create(proto);

console.log(Object.getPrototypeOf(obj) === proto);          // true
console.log(Object.hasOwn(obj, '__proto__'));               // false
console.log(Object.hasOwn(Object.prototype, '__proto__'));  // true

const bare = Object.create(null);
console.log(bare.__proto__);                                // undefined

const lit = { __proto__: proto };
console.log(Object.getPrototypeOf(lit) === proto);          // true

A chave { __proto__: value } em um object literal é uma sintaxe padrão, distinta do acessor, e não está obsoleta. Somente a forma com dois-pontos define o protótipo. Uma chave computada ["__proto__"] cria uma propriedade própria comum.

Mesmo com a API adequada, evite alterar o protótipo depois que o objeto foi criado. A MDN alerta que fazer isso é “atualmente uma operação muito lenta em todos os navegadores e engines JavaScript”. Prefira definir o protótipo no momento da criação do objeto.

Sombreamento: propriedades próprias ocultam as herdadas

A busca para na primeira correspondência, então uma propriedade própria oculta qualquer propriedade de mesmo nome mais acima na cadeia. A atribuição funciona da mesma forma. A menos que um setter herdado ou uma propriedade herdada não gravável interfira, o algoritmo OrdinarySet da especificação cria ou atualiza uma propriedade própria no objeto de destino. A propriedade herdada é sombreada, não alterada.

const defaults = { theme: 'light' };
const settings = Object.create(defaults);

console.log(settings.theme);                    // 'light' (inherited)
settings.theme = 'dark';                        // creates an own property
console.log(defaults.theme);                    // 'light' (prototype untouched)
console.log(Object.hasOwn(settings, 'theme'));  // true

delete settings.theme;
console.log(settings.theme);                    // 'light' (inherited again)
// settings -> defaults -> Object.prototype -> null

Object.hasOwn() indica qual camada respondeu a uma leitura. A MDN o recomenda no lugar de hasOwnProperty().

Como os métodos de Array.prototype chegam a todos os arrays?

Todo array criado com um array literal tem Array.prototype como seu [[Prototype]]. É por isso que map, filter e push funcionam em qualquer array sem precisarem ser copiados para ele. Os objetos nativos também dependem do sombreamento: Array.prototype.toString oculta Object.prototype.toString.

const nums = [1, 2, 3];

console.log(Object.getPrototypeOf(nums) === Array.prototype);             // true
console.log(Object.hasOwn(nums, 'map'));                                  // false
console.log(nums.map === Array.prototype.map);                            // true
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype); // true

console.log(nums.toString());                       // '1,2,3'
console.log(Object.prototype.toString.call(nums));  // '[object Array]'
// nums -> Array.prototype -> Object.prototype -> null

Object.create(null) para objetos sem cadeia

Object.create(null) cria um objeto cujo [[Prototype]] é null. Ele não herda nada: nem toString, nem hasOwnProperty, nem o acessor __proto__. Por isso, um objeto com protótipo null é um contêiner seguro para chaves vindas de entrada do usuário. Com um {} comum, nomes herdados como constructor já retornam um valor antes mesmo de você armazenar qualquer coisa.

const words = ['a', 'constructor', 'a'];

const plain = {};
for (const w of words) plain[w] = (plain[w] ?? 0) + 1;
console.log(plain.constructor); // a string like "function Object() { [native code] }1"

const counts = Object.create(null);
for (const w of words) counts[w] = (counts[w] ?? 0) + 1;
console.log(counts.constructor);   // 1
console.log(counts.a);             // 2
console.log('toString' in counts); // false
// counts -> null

Um objeto com protótipo null não tem métodos herdados, então chame Object.hasOwn(counts, key) em vez de counts.hasOwnProperty(key). Interpolá-lo em uma template literal lança um TypeError, porque não existe toString para convertê-lo em string. Quando as chaves não forem strings, use um Map.

As classes usam a mesma cadeia de protótipos?

Sim. A sintaxe de classes não introduz um novo modelo de herança. Os métodos declarados no corpo de uma classe ficam em ClassName.prototype, e as instâncias chegam até eles pela mesma busca via [[Prototype]] que qualquer outro objeto. O extends liga o objeto prototype de uma classe ao da classe pai.

class Animal { speak() { return 'sound'; } }
class Dog extends Animal { bark() { return 'woof'; } }
const d = new Dog();

console.log(typeof Dog);                                                // 'function'
console.log(Object.hasOwn(d, 'speak'));                                 // false
console.log(Object.getPrototypeOf(d) === Dog.prototype);                // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
console.log(d.speak());                                                 // 'sound'
// d -> Dog.prototype -> Animal.prototype -> Object.prototype -> null

Conclusão

Toda leitura de propriedade segue o mesmo caminho. O JavaScript verifica o próprio objeto e, depois, cada [[Prototype]] em sequência, até encontrar a propriedade ou chegar a null. As escritas ficam no objeto ao qual você atribui o valor. Na próxima vez que um valor parecer ter surgido do nada, execute Object.hasOwn() no objeto, chame Object.getPrototypeOf() em cada elo e anote a cadeia até encontrar o objeto que realmente contém a propriedade.

Perguntas frequentes

Qual é a diferença entre o operador in e Object.hasOwn()?

O operador in verifica toda a cadeia de protótipos, então 'toString' in {} retorna true, porque toString é herdado de Object.prototype. Object.hasOwn(obj, key) verifica apenas as propriedades próprias do objeto e retorna false para qualquer propriedade herdada. Use in quando uma propriedade herdada também deve contar, e Object.hasOwn() quando precisar saber se a chave pertence ao próprio objeto.

Como o instanceof usa a cadeia de protótipos?

Por padrão, obj instanceof C retorna true se C.prototype aparecer em qualquer ponto da cadeia de protótipos de obj. Isso não prova que C criou o objeto, porque qualquer objeto cuja cadeia inclua C.prototype passa na verificação. Uma classe pode substituir essa verificação definindo um método estático Symbol.hasInstance. Com a verificação padrão, valores primitivos do lado esquerdo sempre retornam false.

Loops for...in e Object.keys() incluem propriedades herdadas?

Um loop for...in percorre as propriedades enumeráveis com chave de string do objeto e de todos os objetos acima dele na cadeia de protótipos. Object.keys(), Object.entries() e o spread de objetos incluem apenas as propriedades enumeráveis próprias do objeto. Os métodos de protótipos nativos, como Object.prototype e Array.prototype, não são enumeráveis, então não aparecem no for...in. Já as propriedades enumeráveis dos seus próprios objetos de protótipo aparecem.

Uma cadeia de protótipos do JavaScript pode conter um ciclo?

Não com objetos comuns. Object.setPrototypeOf() lança um TypeError se o novo protótipo criar um ciclo, por exemplo, ao definir o protótipo de a como b quando b já herda de a. Atribuir pelo setter __proto__ lança o mesmo erro. Como os ciclos são rejeitados, uma cadeia de objetos comuns é sempre finita e termina em null, então a busca por uma propriedade sempre chega ao fim.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.