No artigo anterior vimos como criar classes em JS, incluindo propriedades e métodos públicos, privilegiados e privados. Este artigo explica a herança em Javascript.
Sumário
- É possível estabelecer uma relação de herança entre classes com
NomeCLasseFilha.prototype = new ClassePai();
. - É necessário fazer reset da propriedade
constructor
usandoNomeClasseFilha.prototype.constructor=NomeClasseFilha
. - Podem invocar-se métodos das classes ancestrais que a classe filha tenha modificado usando o método
Function.call()
. - O Javascript não tem suporte para métodos protegidos.
Exemplo
Segue-se um exemplo de herança entre duas classes:
function Mamifero(nome){
this.nome=nome;
this.descendencia=[];
}
Mamifero.prototype.temUmFilho=function(){
var novoFilho=new Mamifero("Filho "+this.nome);
this.descendencia.push(novoFilho);
return novoFilho;
}
Mamifero.prototype.toString=function(){
return '[Mamifero "'+this.nome+'"]';
}
Gato.prototype = new Mamifero(); // É aqui que a herança acontece
Gato.prototype.constructor=Gato; // Caso contrário as instâncias de Gato usariam o construtor de Mamifero
function Gato(nome){
this.nome=nome;
}
Gato.prototype.toString=function(){
return '[Gato "'+this.nome+'"]';
}
var umAnimal = new Mamifero('Sr. Biggles');
var bicho = new Gato('Felix');
alert('umAnimal is '+umAnimal); // resulta em 'umAnimal é [Mamifero "Sr. Biggles"]'
alert('bicho is '+bicho); // resulta em 'bicho é [Gato "Felix"]'
bicho.temUmFilho(); // chama um método herdado de Mamifero
alert(bicho.descendencia.length); // mostra que o gato agora tem um filho
alert(bicho.descendencia[0]); // resulta em '[Mamifero "Filho Felix"]'
Utilização da propriedade .constructor
Atente-se na última linha do exemplo anterior. O filho de um Gato deve ser um Gato, certo? Embora o método temUmFilho()
funcione, esse método cria especificamente um new Mamifero
. Embora pudéssemos criar um novo método temUmFilho()
para a subclasse Gato
como this.descendencia.push(new Gato("Filho "+this.nome))
, seria preferível que a classe ancestral criasse um objeto do tipo correto.
Qualquer instância ou objeto em JS tem uma propriedade chamada constructor
que aponta para a sua classe pai. Por exemplo, umAnimal.constructor==Mamifero
é verdade. Com este conhecimento, podemos rescrever o método temUmFilho()
assim:
Mamifero.prototype.temUmFilho=function(){
var novoFilho=new this.constructor("Filho "+this.nome);
this.descendencia.push(novoFilho);
return novoFilho;
}
...
bicho.temUmFilho(); // O mesmo que antes: chama o método herdado de Mamifero
alert(bicho.descendencia[0]); // Agora resulta em '[Gato "Filho Felix"]'
Invocar ‘super’ métodos
Vamos estender o exemplo para quando os gatinhos filhos são criados, eles ‘miam’ logo assim que nascem. Para isso, vai ser necessário escrever um método próprio Gato.prototype.temUmFilho()
, que seja capaz de invocar o método original Mamifero.prototype.temUmFilho()
:
Gato.prototype.temUmFilho=function(){
Mamifero.prototype.temUmFilho.call(this);
alert("miau!");
}
O código acima pode parecer bizarro. O Javascript não tem a prorpiedade ‘super’, que aponte para a classe pai. Ao invés, usa-se o método call()
de uma Function
do objeto, que permite invocar uma função usando um objeto diferente como contexto para essa função. Se for necessário passar parâmetros para essa função, vão após o ‘this’. Para mais informações sobre o método Function.call()
, ver MSDN docs for call()
.
Criar uma propriedade ‘super’
Em vez de ter que saber que Gato
herda de Mamifero
, e ter que escrever Mamifero.prototype
de cada vez que quisermos invocar um método ancestral, não seria mais interessante ter uma propriedade de gato para a sua superclasse? Os mais familiarizados com linguagens orientadas a objetos seriam tentados a chamar a essa propriedade ‘super’, mas o JS reserva esta palavra para uso futuro. A palavra ‘parent’, embora seja usada nalguns itens do DOM, pode ser usada livremente em JS, portanto vamos usar parent
neste exemplo:
Gato.prototype = new Mamifero();
Gato.prototype.constructor=Gato;
Gato.prototype.parent = Mamifero.prototype;
...
Gato.prototype.temUmFilho=function(){
var osFilhos = this.parent.temUmFilho.call(this);
alert("miau!");
return osFilhos;
}
Falsas classes virtuais puras
Algumas linguagens de programação orientadas a objetos têm o conceito de classe virtual pura, que é uma classe que não pode ser instanciada, mas apenas herdada. Por exemplo, podemos ter a classe CoisaViva
da qual a classe Mamifero
herda, mas não queremos que seja possível instanciar CoisaViva
sem especificar o tipo de coisa. É possível fazer isto em JS criando a classe virtual a partir de um objeto ao invés de uma função.
O exemplo seguinte mostra como isto pode ser usado para simular uma superclasse virtual pura:
CoisaViva = {
nascer : function(){
this.vivo=true;
}
}
...
Mamifero.prototype = CoisaViva;
Mamifero.prototype.parent = CoisaViva; //Nota: não 'CoisaViva.prototype'
Mamifero.prototype.temUmFilho=function(){
this.parent.nascer.call(this);
var novoFilho=new this.constructor("Filho "+this.nome);
this.descendencia.push(novoFilho);
return novoFilho;
}
Com o código acima,, a instrução seguinte var espirito = new CoisaViva()
daria erro, pois CoisaViva
não é uma função, e por isso não pode ser usada como um construtor.
Herança conveniente
Ao invés de escrever 3 linhas de cada vez que queremos que uma classe herde de outra, é mais conveniente esteder o objeto Function para fazer isso por nós:
Function.prototype.herdaDe = function( objetoOuClassePai ){
if ( objetoOuClassePai.constructor == Function )
{
//Herança normal
this.prototype = new objetoOuClassePai;
this.prototype.constructor = this;
this.prototype.parent = objetoOuClassePai.prototype;
}
else
{
//Herança pura virtual
this.prototype = objetoOuClassePai;
this.prototype.constructor = this;
this.prototype.parent = objetoOuClassePai;
}
return this;
}
//
//
CoisaViva = {
beBorn : function(){
this.vivo = true;
}
}
//
//
function Mamifero(nome){
this.nome=nome;
this.descendencia=[];
}
Mamifero.herdaDe( CoisaViva );
Mamifero.prototype.temUmFilho=function(){
this.parent.nascer.call(this);
var novoFilho = new this.constructor( "Filho " + this.nome );
this.descendencia.push(novoFilho);
return novoFilho;
}
//
//
function Gato( nome ){
this.nome=nome;
}
Gato.herdaDe( Mamifero );
Gato.prototype.temUmFilho=function(){
var osGatos = this.parent.temUmFilho.call(this);
alert("miau!");
return osGatos;
}
Gato.prototype.toString=function(){
return '[Gato "'+this.nome+'"]';
}
//
//
var felix = new Gato( "Felix" );
var gatos = felix.temUmFilho( ); // mew!
alert( kitten ); // [Gato "Filho Felix"]
Este método deve ser chamado imediatamente após o construtor, e antes de estender o protótipo para o objeto.
Métodos protegidos?
Algumas linguagens de programação orientadas a objetos têm o conceito de métodos protegidos — métodos que existem numa classe pai ou ancestral e que só podem ser invocados por descendentes do objeto, mas não por objetos externos. O JS não suporta estas funcionalidades. Quem precisar disto, terá que escrever a sua própria framework, garantindo que cada classe tem um ‘parent’ ou uma propriedade semelhante, e sobe a árvore à procura de ancestrais, verificando se o objeto invocador é do mesmo tipo. Fazível, mas não agradável.