Herança de classe é uma maneira de uma classe estender outra classe.
Assim, podemos criar novas funcionalidades além das existentes.
Digamos que temos a classe Animal
:
classe Animal { construtor(nome) { esta.velocidade = 0; este.nome = nome; } correr(velocidade) { this.speed = velocidade; alert(`${this.name} roda com velocidade ${this.speed}.`); } parar() { esta.velocidade = 0; alert(`${this.name} está parado.`); } } deixe animal = new Animal("Meu animal");
Veja como podemos representar graficamente o objeto animal
e a classe Animal
:
…E gostaríamos de criar outra class Rabbit
.
Como os coelhos são animais, a classe Rabbit
deve ser baseada em Animal
, ter acesso a métodos animais, para que os coelhos possam fazer o que os animais “genéricos” podem fazer.
A sintaxe para estender outra classe é: class Child extends Parent
.
Vamos criar class Rabbit
que herda de Animal
:
classe Coelho estende Animal { esconder() { alert(`${this.name} esconde!`); } } deixe coelho = novo Coelho("Coelho Branco"); coelho.run(5); // Coelho Branco corre com velocidade 5. coelho.esconder(); // Coelho Branco se esconde!
Os objetos da classe Rabbit
têm acesso tanto aos métodos Rabbit
, como rabbit.hide()
, quanto aos métodos Animal
, como rabbit.run()
.
Internamente, a palavra-chave extends
funciona usando a boa e velha mecânica de protótipo. Ele define Rabbit.prototype.[[Prototype]]
como Animal.prototype
. Portanto, se um método não for encontrado em Rabbit.prototype
, o JavaScript o retirará de Animal.prototype
.
Por exemplo, para encontrar o método rabbit.run
, o mecanismo verifica (de baixo para cima na imagem):
O objeto rabbit
(não tem run
).
Seu protótipo, que é Rabbit.prototype
(tem hide
, mas não run
).
Seu protótipo, que é (devido ao extends
) Animal.prototype
, que finalmente possui o método run
.
Como podemos lembrar do capítulo Protótipos nativos, o próprio JavaScript usa herança prototípica para objetos integrados. Por exemplo, Date.prototype.[[Prototype]]
é Object.prototype
. É por isso que as datas têm acesso a métodos de objetos genéricos.
Qualquer expressão é permitida após extends
A sintaxe da classe permite especificar não apenas uma classe, mas qualquer expressão após extends
.
Por exemplo, uma chamada de função que gera a classe pai:
função f(frase) { classe de retorno { digaOi() { alerta(frase); } }; } class Usuário estende f("Olá") {} novo usuário().sayHi(); // Olá
Aqui class User
herda do resultado de f("Hello")
.
Isso pode ser útil para padrões de programação avançados quando usamos funções para gerar classes dependendo de muitas condições e podemos herdar delas.
Agora vamos seguir em frente e substituir um método. Por padrão, todos os métodos que não são especificados na class Rabbit
são obtidos diretamente “como estão” da class Animal
.
Mas se especificarmos nosso próprio método em Rabbit
, como stop()
então ele será usado:
classe Coelho estende Animal { parar() { // ...agora isso será usado para Rabbit.stop() // em vez de stop() da classe Animal } }
Normalmente, porém, não queremos substituir totalmente um método pai, mas sim construir sobre ele para ajustar ou estender sua funcionalidade. Fazemos algo em nosso método, mas chamamos o método pai antes/depois ou no processo.
As aulas fornecem a palavra-chave "super"
para isso.
super.method(...)
para chamar um método pai.
super(...)
para chamar um construtor pai (apenas dentro do nosso construtor).
Por exemplo, deixe nosso coelho se esconder automaticamente quando parado:
classe Animal { construtor(nome) { esta.velocidade = 0; este.nome = nome; } correr(velocidade) { this.speed = velocidade; alert(`${this.name} roda com velocidade ${this.speed}.`); } parar() { esta.velocidade = 0; alert(`${this.name} está parado.`); } } classe Coelho estende Animal { esconder() { alert(`${this.name} esconde!`); } parar() { super.parar(); //chama parada pai this.hide(); // e depois ocultar } } deixe coelho = novo Coelho("Coelho Branco"); coelho.run(5); // Coelho Branco corre com velocidade 5. coelho.stop(); // Coelho Branco fica parado. O Coelho Branco se esconde!
Agora Rabbit
tem o método stop
que chama o pai super.stop()
no processo.
Funções de seta não têm super
Como foi mencionado no capítulo Funções de seta revisitadas, as funções de seta não possuem super
.
Se acessado, será retirado da função externa. Por exemplo:
classe Coelho estende Animal { parar() { setTimeout(() => super.stop(), 1000); // chama a parada pai após 1 segundo } }
O super
na função de seta é o mesmo que em stop()
, portanto funciona conforme o esperado. Se especificássemos uma função “normal” aqui, haveria um erro:
// Super inesperado setTimeout(function() { super.stop() }, 1000);
Com construtores fica um pouco complicado.
Até agora, Rabbit
não tinha seu próprio constructor
.
De acordo com a especificação, se uma classe estende outra classe e não possui constructor
, então o seguinte constructor
“vazio” é gerado:
classe Coelho estende Animal { //gerado para estender classes sem construtores próprios construtor(...args) { super(...argumentos); } }
Como podemos ver, basicamente chama o constructor
pai passando todos os argumentos. Isso acontece se não escrevermos um construtor próprio.
Agora vamos adicionar um construtor personalizado ao Rabbit
. Ele especificará earLength
além de name
:
classe Animal { construtor(nome) { esta.velocidade = 0; este.nome = nome; } // ... } classe Coelho estende Animal { construtor(nome, earLength) { esta.velocidade = 0; este.nome = nome; this.earLength = earLength; } // ... } //Não funciona! deixe coelho = novo Coelho("Coelho Branco", 10); // Erro: isto não está definido.
Opa! Temos um erro. Agora não podemos criar coelhos. O que deu errado?
A resposta curta é:
Construtores em classes herdadas devem chamar super(...)
e (!) fazer isso antes de usar this
.
…Mas por que? O que está acontecendo aqui? Na verdade, a exigência parece estranha.
Claro, há uma explicação. Vamos entrar em detalhes, para que você realmente entenda o que está acontecendo.
Em JavaScript, há uma distinção entre uma função construtora de uma classe herdada (o chamado “construtor derivado”) e outras funções. Um construtor derivado possui uma propriedade interna especial [[ConstructorKind]]:"derived"
. Esse é um rótulo interno especial.
Esse rótulo afeta seu comportamento com new
.
Quando uma função regular é executada com new
, ela cria um objeto vazio e o atribui a this
.
Mas quando um construtor derivado é executado, isso não acontece. Ele espera que o construtor pai faça esse trabalho.
Portanto, um construtor derivado deve chamar super
para executar seu construtor pai (base), caso contrário o objeto para this
não será criado. E teremos um erro.
Para que o construtor Rabbit
funcione, ele precisa chamar super()
antes de usar this
, como aqui:
classe Animal { construtor(nome) { esta.velocidade = 0; este.nome = nome; } // ... } classe Coelho estende Animal { construtor(nome, earLength) { super(nome); this.earLength = earLength; } // ... } //agora tudo bem deixe coelho = novo Coelho("Coelho Branco", 10); alerta(coelho.nome); // Coelho Branco alerta(coelho.earLength); //10
Nota avançada
Esta nota pressupõe que você tenha alguma experiência com classes, talvez em outras linguagens de programação.
Ele fornece uma visão melhor da linguagem e também explica o comportamento que pode ser uma fonte de bugs (mas não com muita frequência).
Se você achar difícil de entender, continue lendo e retorne algum tempo depois.
Podemos substituir não apenas métodos, mas também campos de classe.
Embora haja um comportamento complicado quando acessamos um campo substituído no construtor pai, bem diferente da maioria das outras linguagens de programação.
Considere este exemplo:
classe Animal { nome = 'animal'; construtor() { alerta(este.nome); // (*) } } classe Coelho estende Animal { nome = 'coelho'; } novo Animal(); //animal novo Coelho(); //animal
Aqui, a classe Rabbit
estende Animal
e substitui o campo name
por seu próprio valor.
Não há construtor próprio em Rabbit
, então o construtor Animal
é chamado.
O interessante é que em ambos os casos: new Animal()
e new Rabbit()
, o alert
na linha (*)
mostra animal
.
Em outras palavras, o construtor pai sempre usa seu próprio valor de campo, e não o valor substituído.
O que há de estranho nisso?
Se ainda não estiver claro, compare com os métodos.
Aqui está o mesmo código, mas em vez do campo this.name
chamamos o método this.showName()
:
classe Animal { showName() { // em vez de this.name = 'animal' alerta('animal'); } construtor() { this.showName(); // em vez de alert(this.name); } } classe Coelho estende Animal { mostrarNome() { alerta('coelho'); } } novo Animal(); //animal novo Coelho(); // coelho
Observação: agora a saída é diferente.
E é isso que esperamos naturalmente. Quando o construtor pai é chamado na classe derivada, ele usa o método substituído.
…Mas para campos de classe não é assim. Como dito, o construtor pai sempre usa o campo pai.
Por que há uma diferença?
Bem, o motivo é a ordem de inicialização dos campos. O campo de classe é inicializado:
Antes do construtor da classe base (que não estende nada),
Imediatamente após super()
para a classe derivada.
No nosso caso, Rabbit
é a classe derivada. Não há constructor()
nele. Como dito anteriormente, é o mesmo que se houvesse um construtor vazio com apenas super(...args)
.
Portanto, new Rabbit()
chama super()
, executando assim o construtor pai, e (de acordo com a regra para classes derivadas) somente depois que seus campos de classe são inicializados. No momento da execução do construtor pai, ainda não há campos da classe Rabbit
, por isso os campos Animal
são usados.
Essa diferença sutil entre campos e métodos é específica do JavaScript.
Felizmente, esse comportamento só se revela se um campo substituído for usado no construtor pai. Então pode ser difícil entender o que está acontecendo, por isso estamos explicando aqui.
Se isso se tornar um problema, pode-se corrigi-lo usando métodos ou getters/setters em vez de campos.
Informações avançadas
Se você estiver lendo o tutorial pela primeira vez – esta seção pode ser ignorada.
É sobre os mecanismos internos por trás da herança e super
.
Vamos nos aprofundar um pouco mais nos bastidores do super
. Veremos algumas coisas interessantes ao longo do caminho.
Em primeiro lugar, de tudo o que aprendemos até agora, é impossível que super
funcione!
Sim, de fato, vamos nos perguntar: como isso deveria funcionar tecnicamente? Quando um método de objeto é executado, ele obtém o objeto atual como this
. Se chamarmos super.method()
então, o mecanismo precisará obter o method
do protótipo do objeto atual. Mas como?
A tarefa pode parecer simples, mas não é. O mecanismo conhece o objeto atual this
, então pode obter o method
pai como this.__proto__.method
. Infelizmente, uma solução tão “ingênua” não funcionará.
Vamos demonstrar o problema. Sem classes, usando objetos simples por uma questão de simplicidade.
Você pode pular esta parte e ir abaixo para a subseção [[HomeObject]]
se não quiser saber os detalhes. Isso não fará mal. Ou continue lendo se estiver interessado em entender as coisas em profundidade.
No exemplo abaixo, rabbit.__proto__ = animal
. Agora vamos tentar: em rabbit.eat()
chamaremos animal.eat()
, usando this.__proto__
:
deixe animal = { nome: "Animal", comer() { alert(`${this.name} come.`); } }; deixe coelho = { __proto__: animal, nome: "Coelho", comer() { // é assim que super.eat() poderia funcionar isto.__proto__.eat.call(isto); // (*) } }; coelho.comer(); // Coelho come.
Na linha (*)
pegamos eat
do protótipo ( animal
) e o chamamos no contexto do objeto atual. Observe que .call(this)
é importante aqui, porque um simples this.__proto__.eat()
executaria parent eat
no contexto do protótipo, não no objeto atual.
E no código acima realmente funciona como pretendido: temos o alert
correto.
Agora vamos adicionar mais um objeto à cadeia. Veremos como as coisas quebram:
deixe animal = { nome: "Animal", comer() { alert(`${this.name} come.`); } }; deixe coelho = { __proto__: animal, comer() { // ...salta no estilo coelho e chama o método pai (animal) isto.__proto__.eat.call(isto); // (*) } }; deixe longEar = { __proto__: coelho, comer() { // ...faça algo com orelhas compridas e chame o método pai (coelho) isto.__proto__.eat.call(isto); // (**) } }; longEar.eat(); // Erro: tamanho máximo da pilha de chamadas excedido
O código não funciona mais! Podemos ver o erro ao tentar chamar longEar.eat()
.
Pode não ser tão óbvio, mas se rastrearmos a chamada longEar.eat()
, poderemos ver o porquê. Em ambas as linhas (*)
e (**)
o valor this
é o objeto atual ( longEar
). Isso é essencial: todos os métodos de objeto obtêm o objeto atual como this
, não como um protótipo ou algo assim.
Então, em ambas as linhas (*)
e (**)
o valor de this.__proto__
é exatamente o mesmo: rabbit
. Ambos chamam rabbit.eat
sem subir na cadeia no loop infinito.
Aqui está a imagem do que acontece:
Dentro de longEar.eat()
, a linha (**)
chama rabbit.eat
fornecendo this=longEar
.
// dentro de longEar.eat() temos this = longEar this.__proto__.eat.call(this) // (**) // torna-se longEar.__proto__.eat.call(este) // aquilo é coelho.eat.call(isto);
Então, na linha (*)
de rabbit.eat
, gostaríamos de passar a chamada ainda mais alto na cadeia, mas this=longEar
, então this.__proto__.eat
é novamente rabbit.eat
!
// dentro de Rabbit.eat() também temos this = longEar this.__proto__.eat.call(this) // (*) // torna-se longEar.__proto__.eat.call(este) // ou (novamente) coelho.eat.call(este);
…Então rabbit.eat
chama a si mesmo no loop infinito, porque não pode subir mais.
O problema não pode ser resolvido usando apenas this
.
[[HomeObject]]
Para fornecer a solução, o JavaScript adiciona mais uma propriedade interna especial para funções: [[HomeObject]]
.
Quando uma função é especificada como uma classe ou método de objeto, sua propriedade [[HomeObject]]
se torna esse objeto.
Então super
usa-o para resolver o protótipo pai e seus métodos.
Vamos ver como funciona, primeiro com objetos simples:
deixe animal = { nome: "Animal", comer() { // animal.eat.[[HomeObject]] == animal alert(`${this.name} come.`); } }; deixe coelho = { __proto__: animal, nome: "Coelho", eat() { // coelho.eat.[[HomeObject]] == coelho super.comer(); } }; deixe longEar = { __proto__: coelho, nome: "Orelha Longa", comer() { // longEar.eat.[[HomeObject]] == longEar super.comer(); } }; //funciona corretamente longEar.eat(); // Orelha Longa come.
Funciona como pretendido, devido à mecânica [[HomeObject]]
. Um método, como longEar.eat
, conhece seu [[HomeObject]]
e pega o método pai de seu protótipo. Sem qualquer uso this
.
Como já sabemos, geralmente as funções são “livres”, não vinculadas a objetos em JavaScript. Assim eles podem ser copiados entre objetos e chamados com outro this
.
A própria existência de [[HomeObject]]
viola esse princípio, porque os métodos lembram seus objetos. [[HomeObject]]
não pode ser alterado, então esse vínculo é para sempre.
O único lugar na linguagem onde [[HomeObject]]
é usado – é super
. Portanto, se um método não usa super
, ainda podemos considerá-lo livre e copiar entre objetos. Mas com super
coisas podem dar errado.
Aqui está a demonstração de um super
errado após a cópia:
deixe animal = { digaOi() { alert(`Eu sou um animal`); } }; //coelho herda de animal deixe coelho = { __proto__: animal, digaOi() { super.sayHi(); } }; deixe plantar = { digaOi() { alert("Eu sou uma planta"); } }; //árvore herda da planta deixe árvore = { __proto__: planta, digaOi: coelho.sayHi // (*) }; tree.sayHi(); //Eu sou um animal (?!?)
Uma chamada para tree.sayHi()
mostra “Eu sou um animal”. Definitivamente errado.
A razão é simples:
Na linha (*)
, o método tree.sayHi
foi copiado de rabbit
. Talvez quiséssemos apenas evitar a duplicação de código?
Seu [[HomeObject]]
é rabbit
, pois foi criado em rabbit
. Não há como alterar [[HomeObject]]
.
O código de tree.sayHi()
contém super.sayHi()
. Sobe do rabbit
e toma o método do animal
.
Aqui está o diagrama do que acontece:
[[HomeObject]]
é definido para métodos tanto em classes quanto em objetos simples. Mas para objetos, os métodos devem ser especificados exatamente como method()
, não como "method: function()"
.
A diferença pode não ser essencial para nós, mas é importante para JavaScript.
No exemplo abaixo, uma sintaxe que não é de método é usada para comparação. A propriedade [[HomeObject]]
não está definida e a herança não funciona:
deixe animal = { eat: function() { // escrevendo intencionalmente assim em vez de eat() {... // ... } }; deixe coelho = { __proto__: animal, comer: função() { super.comer(); } }; coelho.comer(); // Erro ao chamar super (porque não há [[HomeObject]])
Para estender uma classe: class Child extends Parent
:
Isso significa que Child.prototype.__proto__
será Parent.prototype
, portanto os métodos são herdados.
Ao substituir um construtor:
Devemos chamar o construtor pai como super()
no construtor Child
antes de usar this
.
Ao substituir outro método:
Podemos usar super.method()
em um método Child
para chamar o método Parent
.
Internos:
Os métodos lembram sua classe/objeto na propriedade interna [[HomeObject]]
. É assim que super
resolve os métodos pais.
Portanto, não é seguro copiar um método com super
de um objeto para outro.
Também:
As funções de seta não possuem this
ou super
, portanto, elas se ajustam de forma transparente ao contexto circundante.
importância: 5
Aqui está o código com Rabbit
estendendo Animal
.
Infelizmente, os objetos Rabbit
não podem ser criados. O que está errado? Corrija isso.
classe Animal { construtor(nome) { este.nome = nome; } } classe Coelho estende Animal { construtor(nome) { este.nome = nome; this.created = Date.now(); } } deixe coelho = novo Coelho("Coelho Branco"); // Erro: isso não está definido alerta(coelho.nome);
Isso ocorre porque o construtor filho deve chamar super()
.
Aqui está o código corrigido:
classe Animal { construtor(nome) { este.nome = nome; } } classe Coelho estende Animal { construtor(nome) { super(nome); this.created = Date.now(); } } deixe coelho = novo Coelho("Coelho Branco"); //ok agora alerta(coelho.nome); // Coelho Branco
importância: 5
Temos uma aula Clock
. A partir de agora, ele imprime a hora a cada segundo.
classe Relógio { construtor({modelo}) { este.template = modelo; } renderizar() { deixe data = new Data(); deixe horas = date.getHours(); if (horas <10) horas = '0' + horas; deixe minutos = date.getMinutes(); if (minutos <10) minutos = '0' + minutos; deixe segundos = date.getSeconds(); if (seg <10) segundos = '0' + segundos; deixe saída = this.template .replace('h', horas) .replace('m', minutos) .replace('s', segundos); console.log(saída); } parar() { clearInterval(this.timer); } começar() { this.render(); this.timer = setInterval(() => this.render(), 1000); } }
Crie uma nova classe ExtendedClock
que herda de Clock
e adiciona o parâmetro precision
– o número de ms
entre “ticks”. Deve ser 1000
(1 segundo) por padrão.
Seu código deve estar no arquivo extended-clock.js
Não modifique o clock.js
original. Estenda-o.
Abra uma sandbox para a tarefa.
classe ExtendedClock estende Relógio { construtor(opções) { super(opções); deixe {precisão = 1000} = opções; this.precision = precisão; } começar() { this.render(); this.timer = setInterval(() => this.render(), this.precision); } };
Abra a solução em uma sandbox.