Na programação, muitas vezes queremos pegar algo e estendê-lo.
Por exemplo, temos um objeto user
com suas propriedades e métodos e queremos tornar admin
e guest
variantes ligeiramente modificadas dele. Gostaríamos de reutilizar o que temos em user
, não copiar/reimplementar seus métodos, apenas construir um novo objeto sobre ele.
A herança prototípica é um recurso da linguagem que ajuda nisso.
Em JavaScript, os objetos têm uma propriedade oculta especial [[Prototype]]
(conforme nomeado na especificação), que é null
ou faz referência a outro objeto. Esse objeto é chamado de “um protótipo”:
Quando lemos uma propriedade de object
e ela está faltando, o JavaScript a retira automaticamente do protótipo. Em programação, isso é chamado de “herança prototípica”. E em breve estudaremos muitos exemplos de tal herança, bem como recursos de linguagem mais interessantes construídos sobre ela.
A propriedade [[Prototype]]
é interna e oculta, mas existem várias maneiras de configurá-la.
Uma delas é usar o nome especial __proto__
, assim:
deixe animal = { come: verdade }; deixe coelho = { saltos: verdadeiro }; coelho.__proto__ = animal; // define coelho.[[Protótipo]] = animal
Agora, se lermos uma propriedade de rabbit
, e ela estiver faltando, o JavaScript a retirará automaticamente de animal
.
Por exemplo:
deixe animal = { come: verdade }; deixe coelho = { saltos: verdadeiro }; coelho.__proto__ = animal; // (*) // podemos encontrar ambas as propriedades em coelho agora: alerta(coelho.eats); // verdadeiro (**) alerta(coelho.jumps); // verdadeiro
Aqui a linha (*)
define animal
como o protótipo de rabbit
.
Então, quando alert
tenta ler a propriedade rabbit.eats
(**)
, ela não está em rabbit
, então o JavaScript segue a referência [[Prototype]]
e a encontra em animal
(olhe de baixo para cima):
Aqui podemos dizer que “ animal
é o protótipo do rabbit
” ou “ rabbit
herda prototipicamente do animal
”.
Portanto, se animal
tiver muitas propriedades e métodos úteis, eles se tornarão automaticamente disponíveis no rabbit
. Essas propriedades são chamadas de “herdadas”.
Se tivermos um método em animal
, ele pode ser chamado em rabbit
:
deixe animal = { come: verdade, andar() { alert("Passeio dos animais"); } }; deixe coelho = { saltos: verdadeiro, __proto__: animal }; //walk é retirado do protótipo coelho.walk(); // Caminhada de animais
O método é retirado automaticamente do protótipo, assim:
A cadeia de protótipos pode ser mais longa:
deixe animal = { come: verdade, andar() { alert("Passeio dos animais"); } }; deixe coelho = { saltos: verdadeiro, __proto__: animal }; deixe longEar = { comprimento da orelha: 10, __proto__: coelho }; //walk é retirado da cadeia de protótipos longEar.walk(); // Caminhada de animais alerta(longEar.jumps); // verdadeiro (do coelho)
Agora, se lermos algo de longEar
e estiver faltando, o JavaScript irá procurar por isso em rabbit
e depois em animal
.
Existem apenas duas limitações:
As referências não podem andar em círculos. JavaScript gerará um erro se tentarmos atribuir __proto__
em um círculo.
O valor de __proto__
pode ser um objeto ou null
. Outros tipos são ignorados.
Também pode ser óbvio, mas ainda assim: só pode haver um [[Prototype]]
. Um objeto não pode herdar de outros dois.
__proto__
é um getter/setter histórico para [[Prototype]]
É um erro comum de desenvolvedores novatos não saberem a diferença entre os dois.
Observe que __proto__
não é o mesmo que a propriedade interna [[Prototype]]
. É um getter/setter para [[Prototype]]
. Mais tarde veremos situações em que isso é importante, por enquanto vamos apenas manter isso em mente, à medida que construímos nossa compreensão da linguagem JavaScript.
A propriedade __proto__
está um pouco desatualizada. Ele existe por razões históricas, o JavaScript moderno sugere que devemos usar as funções Object.getPrototypeOf/Object.setPrototypeOf
em vez de obter/definir o protótipo. Também abordaremos essas funções mais tarde.
Pela especificação, __proto__
deve ser suportado apenas por navegadores. Na verdade, porém, todos os ambientes, incluindo o lado do servidor, suportam __proto__
, então estamos bastante seguros ao usá-lo.
Como a notação __proto__
é um pouco mais intuitivamente óbvia, nós a usamos nos exemplos.
O protótipo é usado apenas para leitura de propriedades.
As operações de gravação/exclusão funcionam diretamente com o objeto.
No exemplo abaixo, atribuímos seu próprio método walk
a rabbit
:
deixe animal = { come: verdade, andar() { /* este método não será usado pelo coelho */ } }; deixe coelho = { __proto__: animal }; coelho.walk = function() { alert("Coelho! Pula-pula!"); }; coelho.walk(); // Coelho! Salte-salte!
A partir de agora, a chamada rabbit.walk()
encontra o método imediatamente no objeto e o executa, sem usar o protótipo:
As propriedades do acessador são uma exceção, pois a atribuição é tratada por uma função setter. Portanto, escrever nessa propriedade é, na verdade, o mesmo que chamar uma função.
Por esse motivo admin.fullName
funciona corretamente no código abaixo:
deixe usuário = { nome: "João", sobrenome: "Smith", definir nome completo(valor) { [este.nome, este.sobrenome] = valor.split(" "); }, obterNomeCompleto() { return `${este.nome} ${este.sobrenome}`; } }; deixe administrador = { __proto__: usuário, isAdmin: verdadeiro }; alerta(admin.nome completo); //João Smith (*) // gatilhos setter! admin.fullName = "Alice Cooper"; // (**) alerta(admin.nome completo); // Alice Cooper, estado do administrador modificado alerta(usuário.nome completo); // John Smith, estado do usuário protegido
Aqui na linha (*)
a propriedade admin.fullName
possui um getter no protótipo user
, por isso é chamado. E na linha (**)
a propriedade possui um setter no protótipo, por isso é chamada.
Uma questão interessante pode surgir no exemplo acima: qual é o valor this
set fullName(value)
? Onde estão as propriedades this.name
e this.surname
escritas: em user
ou admin
?
A resposta é simples: this
não é afetado de forma alguma pelos protótipos.
Não importa onde o método seja encontrado: em um objeto ou em seu protótipo. Em uma chamada de método, this
é sempre o objeto antes do ponto.
Portanto, a chamada do setter admin.fullName=
usa admin
como this
, não user
.
Na verdade, isso é algo superimportante, porque podemos ter um objeto grande com muitos métodos e ter objetos que herdam dele. E quando os objetos herdados executam os métodos herdados, eles modificarão apenas seus próprios estados, não o estado do objeto grande.
Por exemplo, aqui animal
representa um “armazenamento de método” e rabbit
faz uso dele.
A chamada rabbit.sleep()
define this.isSleeping
no objeto rabbit
:
//animal tem métodos deixe animal = { andar() { if (!this.isSleeping) { alert(`Eu ando`); } }, dormir() { this.isSleeping=true; } }; deixe coelho = { nome: "Coelho Branco", __proto__: animal }; // modifica coelho.isSleeping coelho.sleep(); alerta(coelho.isSleeping); // verdadeiro alerta(animal.isSleeping); // indefinido (não existe tal propriedade no protótipo)
A imagem resultante:
Se tivéssemos outros objetos, como bird
, snake
, etc., herdados de animal
, eles também teriam acesso aos métodos de animal
. Mas this
em cada chamada de método seria o objeto correspondente, avaliado no momento da chamada (antes do ponto), não animal
. Então, quando escrevemos dados this
, eles são armazenados nesses objetos.
Como resultado, os métodos são compartilhados, mas o estado do objeto não.
O loop for..in
também itera sobre propriedades herdadas.
Por exemplo:
deixe animal = { come: verdade }; deixe coelho = { saltos: verdadeiro, __proto__: animal }; // Object.keys retorna apenas chaves próprias alerta(Object.keys(coelho)); // salta // for..in faz loop sobre chaves próprias e herdadas for(deixe prop entrar coelho) alert(prop); // pula e depois come
Se não é isso que queremos e gostaríamos de excluir propriedades herdadas, existe um método interno obj.hasOwnProperty(key): ele retorna true
se obj
tiver sua própria propriedade (não herdada) chamada key
.
Portanto, podemos filtrar as propriedades herdadas (ou fazer outra coisa com elas):
deixe animal = { come: verdade }; deixe coelho = { saltos: verdadeiro, __proto__: animal }; for(deixe prop no coelho) { deixe isOwn = coelho.hasOwnProperty(prop); if (éOpróprio) { alert(`Nosso: ${prop}`); // Nosso: saltos } outro { alert(`Herdado: ${prop}`); // Herdado: come } }
Aqui temos a seguinte cadeia de herança: rabbit
herda de animal
, que herda de Object.prototype
(porque animal
é um objeto literal {...}
, então é por padrão), e então null
acima dele:
Observe que há uma coisa engraçada. De onde vem o método rabbit.hasOwnProperty
? Nós não definimos isso. Olhando para a cadeia podemos ver que o método é fornecido por Object.prototype.hasOwnProperty
. Em outras palavras, é herdado.
…Mas por que hasOwnProperty
não aparece no loop for..in
como eats
e jumps
, se for..in
lista propriedades herdadas?
A resposta é simples: não é enumerável. Assim como todas as outras propriedades de Object.prototype
, ele possui o sinalizador enumerable:false
. E for..in
lista apenas propriedades enumeráveis. É por isso que ele e o restante das propriedades Object.prototype
não estão listados.
Quase todos os outros métodos de obtenção de chave/valor ignoram propriedades herdadas
Quase todos os outros métodos de obtenção de chave/valor, como Object.keys
, Object.values
e assim por diante, ignoram as propriedades herdadas.
Eles operam apenas no próprio objeto. As propriedades do protótipo não são levadas em consideração.
Em JavaScript, todos os objetos têm uma propriedade [[Prototype]]
oculta que é outro objeto ou null
.
Podemos usar obj.__proto__
para acessá-lo (um getter/setter histórico, existem outras maneiras, que serão abordadas em breve).
O objeto referenciado por [[Prototype]]
é chamado de “protótipo”.
Se quisermos ler uma propriedade de obj
ou chamar um método e ele não existir, o JavaScript tentará encontrá-lo no protótipo.
As operações de gravação/exclusão atuam diretamente no objeto, elas não usam o protótipo (assumindo que é uma propriedade de dados, não um setter).
Se chamarmos obj.method()
, e o method
for retirado do protótipo, this
ainda fará referência obj
. Portanto, os métodos sempre funcionam com o objeto atual, mesmo que sejam herdados.
O loop for..in
itera sobre suas próprias propriedades e sobre suas propriedades herdadas. Todos os outros métodos de obtenção de chave/valor operam apenas no próprio objeto.
importância: 5
Aqui está o código que cria um par de objetos e depois os modifica.
Quais valores são mostrados no processo?
deixe animal = { saltos: nulo }; deixe coelho = { __proto__: animal, saltos: verdadeiro }; alerta(coelho.jumps); // ? (1) exclua coelho.jumps; alerta(coelho.jumps); // ? (2) excluir animal.jumps; alerta(coelho.jumps); // ? (3)
Deve haver 3 respostas.
true
, tirado de rabbit
.
null
, retirado de animal
.
undefined
, não existe mais tal propriedade.
importância: 5
A tarefa tem duas partes.
Dados os seguintes objetos:
deixe cabeça = { óculos: 1 }; deixe tabela = { caneta: 3 }; deixe dormir = { folha: 1, travesseiro: 2 }; deixe bolsos = { dinheiro: 2000 };
Use __proto__
para atribuir protótipos de forma que qualquer pesquisa de propriedade siga o caminho: pockets
→ bed
→ table
→ head
. Por exemplo, pockets.pen
deve ser 3
(encontrado em table
) e bed.glasses
deve ser 1
(encontrado em head
).
Responda à pergunta: é mais rápido conseguir glasses
como pockets.glasses
ou head.glasses
? Referência, se necessário.
Vamos adicionar __proto__
:
deixe cabeça = { óculos: 1 }; deixe tabela = { caneta: 3, __proto__: cabeça }; deixe dormir = { folha: 1, travesseiro: 2, __proto__: tabela }; deixe bolsos = { dinheiro: 2000, __proto__: cama }; alerta(bolsos.caneta); //3 alerta(cama.óculos); //1 alerta(tabela.money); // indefinido
Nos motores modernos, em termos de desempenho, não há diferença se pegamos uma propriedade de um objeto ou de seu protótipo. Eles lembram onde o imóvel foi encontrado e o reutilizam na próxima solicitação.
Por exemplo, para pockets.glasses
eles lembram onde encontraram glasses
(na head
) e da próxima vez pesquisarão ali mesmo. Eles também são inteligentes o suficiente para atualizar caches internos se algo mudar, para que a otimização seja segura.
importância: 5
Temos rabbit
herdando de animal
.
Se chamarmos rabbit.eat()
, qual objeto recebe a propriedade full
: animal
ou rabbit
?
deixe animal = { comer() { isto.completo = verdadeiro; } }; deixe coelho = { __proto__: animal }; coelho.comer();
A resposta: rabbit
.
Isso porque this
é um objeto antes do ponto, então rabbit.eat()
modifica rabbit
.
A pesquisa e a execução de propriedades são duas coisas diferentes.
O método rabbit.eat
é encontrado primeiro no protótipo e depois executado com this=rabbit
.
importância: 5
Temos dois hamsters: speedy
e lazy
herdados do objeto hamster
geral.
Quando alimentamos um deles, o outro também fica satisfeito. Por que? Como podemos consertar isso?
deixe hamster = { estômago: [], comer(comida) { this.stomach.push(comida); } }; deixe rápido = { __proto__: hamster }; deixe preguiçoso = { __proto__: hamster }; // Este aqui encontrou a comida speedy.eat("maçã"); alerta(rápido.estômago); // maçã // Esse aqui também tem, por quê? conserte por favor. alerta( preguiçoso.estômago ); // maçã
Vejamos com atenção o que está acontecendo na chamada speedy.eat("apple")
.
O método speedy.eat
é encontrado no protótipo ( =hamster
), então executado com this=speedy
(o objeto antes do ponto).
Então this.stomach.push()
precisa encontrar a propriedade stomach
e chamar push
nela. Ele procura stomach
this
( =speedy
), mas nada encontrou.
Então ele segue a cadeia do protótipo e encontra stomach
do hamster
.
Em seguida, ele chama de push
, adicionando a comida ao estômago do protótipo .
Então todos os hamsters compartilham um único estômago!
Tanto para lazy.stomach.push(...)
quanto speedy.stomach.push()
, a propriedade stomach
é encontrada no protótipo (já que não está no objeto em si), então os novos dados são inseridos nele.
Observe que isso não acontece no caso de uma atribuição simples this.stomach=
:
deixe hamster = { estômago: [], comer(comida) { // atribui a this.stomach em vez de this.stomach.push this.estômago = [comida]; } }; deixe rápido = { __proto__: hamster }; deixe preguiçoso = { __proto__: hamster }; // O veloz encontrou a comida speedy.eat("maçã"); alerta(rápido.estômago); // maçã // O estômago do preguiçoso está vazio alerta( preguiçoso.estômago ); // <nada>
Agora tudo funciona bem, porque this.stomach=
não realiza uma pesquisa de stomach
. O valor é escrito diretamente this
objeto.
Também podemos evitar totalmente o problema certificando-nos de que cada hamster tenha seu próprio estômago:
deixe hamster = { estômago: [], comer(comida) { this.stomach.push(comida); } }; deixe rápido = { __proto__: hamster, estômago: [] }; deixe preguiçoso = { __proto__: hamster, estômago: [] }; // O veloz encontrou a comida speedy.eat("maçã"); alerta(rápido.estômago); // maçã // O estômago do preguiçoso está vazio alerta( preguiçoso.estômago ); // <nada>
Como solução comum, todas as propriedades que descrevem o estado de um objeto específico, como stomach
acima, devem ser escritas nesse objeto. Isso evita tais problemas.