Herança Prototípica em JavaScript
Aprenda herança prototípica em JavaScript: o link [[Prototype]], __proto__ vs Object.getPrototypeOf/setPrototypeOf, cadeia de protótipos, F.prototype, sombreamento de propriedades e como this se comporta na cadeia.
Em JavaScript, objects podem herdar propriedades e métodos de outros objects por meio de um mecanismo chamado herança prototípica. Em vez de copiar funcionalidades de uma classe (como em Java ou C++), cada object mantém um link oculto para outro object — seu protótipo — e o JavaScript segue esse link sempre que não encontra uma propriedade no próprio object. Esta página aborda como esse link oculto funciona, a diferença entre __proto__ e Object.getPrototypeOf/setPrototypeOf, como a cadeia de protótipos é percorrida, o papel de F.prototype para funções construtoras, sombreamento de propriedades e como this se comporta ao longo da cadeia.
O link oculto [[Prototype]]
Todo object JavaScript possui uma propriedade interna oculta chamada [[Prototype]]. Ela é null ou uma referência a outro object, e esse object referenciado é chamado de protótipo do object.
[[Prototype]] é um slot interno da especificação da linguagem — não é possível lê-lo com um acesso normal a propriedade. Em vez disso, você trabalha com ele por meio de duas APIs públicas:
- O acessor histórico
__proto__(um getter/setter exposto porObject.prototype). - Os métodos modernos e recomendados
Object.getPrototypeOf(obj)eObject.setPrototypeOf(obj, proto).
A maneira mais simples de definir um protótipo é com __proto__ dentro de um object literal. Aqui fazemos animal ser o protótipo de rabbit, para que rabbit possa ler as propriedades de animal:
__proto__ vs Object.getPrototypeOf / setPrototypeOf
__proto__ é um getter/setter que há muito tempo está depreciado para uso geral — ele é padronizado apenas para compatibilidade com navegadores. Em código real, prefira os métodos explícitos:
Note a diferença: __proto__ é um acessor de propriedade, enquanto getPrototypeOf/setPrototypeOf são funções. Duas regras práticas:
__proto__não é o mesmo que[[Prototype]].__proto__é apenas o acessor que lê/escreve o slot interno[[Prototype]].- Evite alterar um protótipo após a criação de um object.
Object.setPrototypeOfe a atribuição deobj.__proto__são operações lentas: os mecanismos de JavaScript otimizam fortemente objects cujo protótipo é fixado no momento da criação. Defina o protótipo uma única vez, ao construir o object.
A cadeia de protótipos
O protótipo de um protótipo também pode ter um protótipo, formando uma cadeia de protótipos. Quando você lê obj.prop, o JavaScript:
- Procura
propcomo propriedade própria deobj. - Se não encontrar, segue
[[Prototype]]e procura no protótipo. - Repete o passo 2 subindo na cadeia até encontrar
propou chegar anull.
Se a cadeia terminar em null sem encontrar a propriedade, o resultado é undefined.
Há dois limites na cadeia: as referências não podem formar um loop (o JavaScript lança um erro se você tentar criar um ciclo), e [[Prototype]] deve ser um object ou null.
Sombreamento de propriedades: escrita vs leitura
O protótipo é consultado apenas para leitura. Quando você escreve ou exclui uma propriedade, a operação sempre age sobre o próprio object, nunca sobre seu protótipo. Atribuir uma propriedade que também existe no protótipo cria uma propriedade própria que sombreia (oculta) a herdada:
A exceção são as propriedades acessoras (getters/setters): como um setter é uma chamada de função, escrever por meio de um setter herdado executa esse setter em vez de criar uma nova propriedade de dados própria.
this é sempre o object que faz a chamada
Uma fonte comum de confusão: não importa onde um método esteja na cadeia, o this dentro dele é o object antes do ponto no momento em que o método foi chamado — nunca o protótipo onde ele foi definido. Métodos herdados, portanto, operam sobre o estado próprio do object herdeiro:
É isso que torna os métodos compartilhados em um protótipo úteis: uma única definição de método, mas cada object armazena seus próprios dados.
Funções construtoras e F.prototype
Definir [[Prototype]] manualmente para cada object é tedioso. O padrão clássico é usar uma função construtora com new. Toda função tem uma propriedade comum chamada prototype (escrita como F.prototype). Quando você chama new F(), o [[Prototype]] do object recém-criado é definido como F.prototype.
Mantenha F.prototype distinto do [[Prototype]] de um object: F.prototype é uma propriedade comum da função construtora que fornece o [[Prototype]] para objects criados com new F(). Por padrão, F.prototype é um object com uma única propriedade não enumerável constructor que aponta de volta para a função em si.
Nota: A sintaxe
classdo ES6 moderno é açúcar sintático sobre exatamente esse mecanismo. Uma declaraçãoclasscria uma função construtora, coloca seus métodos emConstructor.prototypee conecta a cadeia com os mesmos links[[Prototype]]. Veja Herança de Classes em JavaScript para a forma comextends/super.
Criando objects com Object.create
Object.create(proto) constrói um object novo com [[Prototype]] definido diretamente como proto — sem necessidade de construtor. É a maneira mais explícita de configurar herança, e aceita um segundo argumento: um mapa de descritores de propriedades, no mesmo formato usado por Object.defineProperties.
O mapa de descritores permite controlar flags como writable, enumerable e configurable — veja Flags e Descritores de Propriedades em JavaScript para saber o que cada flag faz. Objects criados com Object.create(null) (os chamados objects "muito simples") são abordados em Métodos de Protótipo, Objects Sem __proto__.
Herança multinível
Como cada protótipo pode ter seu próprio protótipo, você pode construir cadeias com vários níveis de profundidade para modelar relacionamentos mais específicos:
Inspecionando e iterando a cadeia
Para verificar se um object está em algum lugar da cadeia de outro object, use isPrototypeOf. Para separar propriedades próprias das herdadas, use hasOwnProperty — note que for...in percorre toda a cadeia (somente propriedades enumeráveis), enquanto Object.keys retorna apenas as chaves próprias.
Tipos embutidos como arrays, funções e datas também dependem dessa cadeia — seus métodos residem em Array.prototype, Function.prototype e assim por diante. Veja Protótipos Nativos em JavaScript para saber como os tipos embutidos estão conectados.
Resumo
- Todo object possui um
[[Prototype]]oculto que é outro object ounull. - Leia
[[Prototype]]comObject.getPrototypeOf, defina-o comObject.setPrototypeOf;__proto__é o acessor legado e alterar um protótipo após a criação é lento. - As leituras percorrem a cadeia de protótipos até a propriedade ser encontrada ou a cadeia terminar em
null; escritas e exclusões sempre agem sobre o próprio object e podem sombrear propriedades herdadas. thisdentro de um método é o object no qual o método foi chamado, não o protótipo onde ele foi definido.new F()define o[[Prototype]]do novo object comoF.prototype; oclassdo ES6 é açúcar sintático sobre exatamente esse mecanismo — veja Herança de Classes em JavaScript.