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.
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 emnull, ela retornaundefined. - A propriedade
prototypede uma função se torna o[[Prototype]]das instâncias criadas comnew. Ela não é o protótipo da própria função. __proto__é um acessor obsoleto herdado deObject.prototype. UseObject.getPrototypeOf()eObject.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.
| Nome | O que é | Onde fica | Como ler |
|---|---|---|---|
[[Prototype]] | O link interno que a busca segue | Em todo objeto (um slot interno) | Object.getPrototypeOf(obj) |
obj.__proto__ | Um getter/setter legado para esse link | Object.prototype | Evite; use getPrototypeOf |
Fn.prototype | O objeto atribuído às instâncias criadas com new Fn() | Funções construtíveis e classes | Fn.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.
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