Herança e polimorfismo
Esta aula aborda os conceitos fundamentais de herança e polimorfismo em JavaScript, incluindo as palavras-chave extends e super, a sobrescrita de métodos e o operador instanceof. Através de exemplos práticos, você aprenderá a criar hierarquias de classes, reutilizar código e escrever programas mais flexíveis e orientados a objetos.
Nesta aula, vamos explorar dois pilares da programação orientada a objetos em JavaScript: herança e polimorfismo. A herança permite que uma classe derive de outra, reutilizando atributos e métodos, enquanto o polimorfismo nos permite tratar objetos de diferentes classes de maneira uniforme, invocando comportamentos específicos de cada uma. Compreender esses conceitos é essencial para criar aplicações escaláveis e bem estruturadas.
Em JavaScript, a herança é implementada através de classes e da palavra-chave extends, que estabelece uma relação "é um" entre classes. A palavra-chave super é usada para acessar o construtor e os métodos da classe pai. A sobrescrita de métodos permite que uma subclasse forneça uma implementação específica de um método herdado. Por fim, o operador instanceof permite verificar se um objeto é uma instância de uma determinada classe (ou de suas subclasses). Vamos detalhar cada um desses tópicos com exemplos práticos.
extends
A palavra-chave extends é usada para criar uma subclasse (classe filha) que herda propriedades e métodos de uma superclasse (classe pai). A sintaxe básica é: class Filha extends Pai { ... }. A subclasse herda todos os métodos e propriedades da superclasse, podendo adicionar novos ou modificar os existentes.
Vejamos um exemplo simples: uma classe Animal e uma subclasse Cachorro que herda dela. A classe Animal pode ter um método falar() que retorna um som genérico, e a classe Cachorro pode adicionar um método específico como buscar().
class Animal {
constructor(nome) {
this.nome = nome;
}
falar() {
return `${this.nome} faz um som`;
}
}
class Cachorro extends Animal {
buscar() {
return `${this.nome} está buscando a bola!`;
}
}
const rex = new Cachorro('Rex');
console.log(rex.falar()); // "Rex faz um som"
console.log(rex.buscar()); // "Rex está buscando a bola!"Observe que rex é uma instância de Cachorro e, portanto, herda o método falar() de Animal. Isso demonstra a reutilização de código e a criação de hierarquias.
super
A palavra-chave super é usada para acessar o construtor e os métodos da classe pai. Dentro do construtor da subclasse, devemos chamar super() antes de usar this, pois isso inicializa as propriedades herdadas. Além disso, podemos usar super.metodo() para chamar um método da superclasse.
No exemplo a seguir, a classe Gato estende Animal e usa super para chamar o construtor da classe pai e também para acessar o método falar():
class Animal {
constructor(nome) {
this.nome = nome;
}
falar() {
return `${this.nome} faz um som`;
}
}
class Gato extends Animal {
constructor(nome, raca) {
super(nome); // chama o construtor da classe pai
this.raca = raca;
}
falar() {
return `${super.falar()} mas especificamente: Miau!`;
}
}
const mingau = new Gato('Mingau', 'Siamês');
console.log(mingau.falar()); // "Mingau faz um som mas especificamente: Miau!"No construtor de Gato, chamamos super(nome) para definir this.nome antes de adicionar a propriedade raca. No método falar(), usamos super.falar() para obter o comportamento da classe pai e estendê-lo. Isso é uma prática comum: chamar o método pai e adicionar lógica extra.
Sobrescrita de métodos
A sobrescrita de métodos (override) ocorre quando uma subclasse define um método com o mesmo nome de um método da superclasse. Isso permite que a subclasse forneça uma implementação específica, alterando ou estendendo o comportamento herdado.
Um exemplo clássico é a classe Pessoa e a classe Aluno. A classe Pessoa tem um método apresentar() que retorna o nome e a idade. A classe Aluno sobrescreve esse método para incluir a matrícula e o curso.
class Pessoa {
constructor(nome, idade) {
this.nome = nome;
this.idade = idade;
}
apresentar() {
return `Olá, eu sou ${this.nome} e tenho ${this.idade} anos.`;
}
}
class Aluno extends Pessoa {
constructor(nome, idade, matricula, curso) {
super(nome, idade);
this.matricula = matricula;
this.curso = curso;
}
apresentar() {
return `${super.apresentar()} Sou aluno do curso de ${this.curso} (matrícula: ${this.matricula}).`;
}
}
const ana = new Aluno('Ana', 20, '12345', 'Engenharia');
console.log(ana.apresentar());
// "Olá, eu sou Ana e tenho 20 anos. Sou aluno do curso de Engenharia (matrícula: 12345)."Note que usamos super.apresentar() para aproveitar o código da classe pai e, em seguida, adicionamos a informação específica do aluno. Isso evita duplicação e mantém a consistência.
instanceof
O operador instanceof verifica se um objeto é uma instância de uma determinada classe (ou de uma de suas subclasses). Ele retorna true se o objeto foi criado com essa classe ou com uma classe derivada. A sintaxe é: objeto instanceof Classe.
É muito útil para realizar verificações de tipo em tempo de execução, especialmente quando lidamos com hierarquias de classes. Por exemplo, podemos verificar se um objeto é um Animal ou um Cachorro:
class Animal {}
class Cachorro extends Animal {}
const rex = new Cachorro();
console.log(rex instanceof Cachorro); // true
console.log(rex instanceof Animal); // true
console.log(rex instanceof Object); // true
const animal = new Animal();
console.log(animal instanceof Cachorro); // falseNo exemplo, rex é instância de Cachorro e também de Animal porque Cachorro estende Animal. Isso é útil para escrever funções que aceitam um tipo base e podem tratar subclasses de forma genérica.
Um padrão comum é usar instanceof para executar ações específicas com base no tipo do objeto:
function descrever(animal) {
if (animal instanceof Cachorro) {
return 'É um cachorro!';
} else if (animal instanceof Animal) {
return 'É um animal genérico.';
}
return 'Tipo desconhecido';
}
console.log(descrever(new Cachorro())); // "É um cachorro!"
console.log(descrever(new Animal())); // "É um animal genérico."Boas práticas
- Prefira composição em vez de herança quando a relação não for claramente "é um". Herança pode criar acoplamento desnecessário.
- Use
supersempre que precisar reutilizar o código da classe pai no construtor ou em métodos sobrescritos, evitando duplicação. - Mantenha as hierarquias rasas para evitar complexidade excessiva e problemas de manutenção.
- Use
instanceofcom moderação; em muitos casos, o polimorfismo (sobrescrita de métodos) é uma alternativa mais elegante que verificação de tipo. - Documente as classes e métodos para que outros desenvolvedores entendam a relação entre as classes.
Exercícios
Crie uma classe base
Veiculocom propriedadesmarcaeano, e um métodoinformacoes()que retorna uma string com essas informações. Crie uma subclasseCarroque adiciona a propriedadeportase sobrescreveinformacoes()para incluir o número de portas. Teste as classes.✓ Resposta:class Veiculo { constructor(marca, ano) { this.marca = marca; this.ano = ano; } informacoes() { return `Marca: ${this.marca}, Ano: ${this.ano}`; } } class Carro extends Veiculo { constructor(marca, ano, portas) { super(marca, ano); this.portas = portas; } informacoes() { return `${super.informacoes()}, Portas: ${this.portas}`; } } const meuCarro = new Carro('Toyota', 2020, 4); console.log(meuCarro.informacoes()); // "Marca: Toyota, Ano: 2020, Portas: 4"Explique o que acontece se você não chamar
super()no construtor de uma subclasse. Dê um exemplo de erro.✓ Resposta: Se você não chamarsuper()em um construtor de subclasse, o JavaScript lançará um erro de referência (ReferenceError) quando você tentar usarthis, porque othisnão foi inicializado. Exemplo:class Animal { constructor(nome) { this.nome = nome; } } class Cachorro extends Animal { constructor(raca) { // Não chama super() this.raca = raca; // ReferenceError: Must call super constructor in derived class before accessing 'this' } }Usando as classes
AnimaleCachorrodo exemplo anterior, crie uma função que receba um objetoAnimale useinstanceofpara retornar uma mensagem específica para cachorros e outra para outros animais.✓ Resposta:class Animal {} class Cachorro extends Animal {} function identificar(animal) { if (animal instanceof Cachorro) { return 'É um cachorro! 🐶'; } else if (animal instanceof Animal) { return 'É um animal genérico.'; } else { return 'Não é um animal.'; } } console.log(identificar(new Cachorro())); // "É um cachorro! 🐶" console.log(identificar(new Animal())); // "É um animal genérico."Escreva um exemplo de polimorfismo onde uma função aceita um array de objetos de classes diferentes (todas derivadas de uma mesma base) e chama um método comum, produzindo saídas diferentes.
✓ Resposta:class Animal { falar() { return 'Som genérico'; } } class Cachorro extends Animal { falar() { return 'Au au!'; } } class Gato extends Animal { falar() { return 'Miau!'; } } function fazerTodosFalarem(animais) { animais.forEach(animal => console.log(animal.falar())); } const animais = [new Cachorro(), new Gato(), new Animal()]; fazerTodosFalarem(animais); // "Au au!" // "Miau!" // "Som genérico"Crie uma classe
ContaBancariacom métodosdepositar(valor)esacar(valor). Em seguida, crie uma subclasseContaPoupancaque sobrescreve o métodosacar()para aplicar uma taxa de 0.5% sobre o valor sacado. Teste o comportamento.✓ Resposta:class ContaBancaria { constructor(saldo) { this.saldo = saldo; } depositar(valor) { this.saldo += valor; } sacar(valor) { if (valor <= this.saldo) { this.saldo -= valor; return true; } return false; } } class ContaPoupanca extends ContaBancaria { sacar(valor) { const taxa = valor * 0.005; const total = valor + taxa; if (total <= this.saldo) { this.saldo -= total; console.log(`Saque de R$ ${valor.toFixed(2)} com taxa de R$ ${taxa.toFixed(2)}`); return true; } return false; } } const conta = new ContaPoupanca(1000); conta.sacar(100); // "Saque de R$ 100.00 com taxa de R$ 0.50" console.log(conta.saldo); // 899.5