Herança e Polimorfismo
Herança em JavaScript: Guia Completo para Iniciantes
E aí, dev! Vamos mergulhar em um dos conceitos mais importantes da programação orientada a objetos: herança. Se você já ouviu falar que JavaScript "não é uma linguagem orientada a objetos de verdade", pode esquecer isso - desde o ES6 (2015), temos suporte completo a classes e herança!
O que é Herança?
Herança é quando uma classe "filha" herda características (propriedades e métodos) de uma classe "pai". Pense nisso como uma família: filhos herdam características dos pais, mas também têm suas próprias particularidades.
// Classe pai (superclasse)
class Animal {
constructor(nome) {
this.nome = nome;
}
fazerSom() {
console.log(`${this.nome} faz um som.`);
}
}
// Classe filha (subclasse)
class Cachorro extends Animal {
// Já herda nome e fazerSom() automaticamente!
}
extends: Criando Herança
A palavra-chave extends é usada para criar uma classe filha. Ela estabelece a relação de herança.
class Animal {
constructor(nome) {
this.nome = nome;
}
}
class Cachorro extends Animal {}
class Gato extends Animal {}
const rex = new Cachorro('Rex');
const mimi = new Gato('Mimi');
console.log(rex.nome); // "Rex"
console.log(mimi.nome); // "Mimi"
// Ambos são instâncias de Animal
console.log(rex instanceof Animal); // true
console.log(mimi instanceof Animal); // true
super(): Chamando o Construtor do Pai
Quando a classe filha tem seu próprio construtor, ela precisa chamar super() para executar o construtor da classe pai. Isso é obrigatório!
class Animal {
constructor(nome) {
this.nome = nome;
this.vivo = true;
}
}
class Cachorro extends Animal {
constructor(nome, raca) {
super(nome); // Chama o construtor de Animal
this.raca = raca; // Propriedade específica do Cachorro
}
}
const rex = new Cachorro('Rex', 'Labrador');
console.log(rex.nome); // "Rex" (herdado)
console.log(rex.vivo); // true (herdado)
console.log(rex.raca); // "Labrador" (próprio)
Erro comum #1: Esquecer de chamar super() no construtor da classe filha!
class Gato extends Animal {
constructor(nome, cor) {
// Esqueceu de chamar super(nome)!
this.cor = cor; // ReferenceError: precisa chamar super primeiro
}
}
Override de Métodos
Você pode sobrescrever métodos da classe pai na classe filha, dando um comportamento diferente.
class Animal {
fazerSom() {
console.log('Som genérico de animal');
}
}
class Cachorro extends Animal {
fazerSom() {
console.log('Au au!');
}
}
class Gato extends Animal {
fazerSom() {
console.log('Miau!');
}
}
const animais = [
new Cachorro(),
new Gato(),
new Animal()
];
animais.forEach(animal => animal.fazerSom());
// "Au au!"
// "Miau!"
// "Som genérico de animal"
Chamando o Método do Pai com super
Às vezes você quer adicionar comportamento ao método do pai, não substituir completamente.
class Animal {
constructor(nome) {
this.nome = nome;
}
apresentar() {
return `Olá, eu sou ${this.nome}`;
}
}
class Cachorro extends Animal {
apresentar() {
const apresentacaoBase = super.apresentar(); // Chama método do pai
return `${apresentacaoBase} e sou um cachorro!`;
}
}
const rex = new Cachorro('Rex');
console.log(rex.apresentar()); // "Olá, eu sou Rex e sou um cachorro!"
instanceof: Verificando o Tipo
O operador instanceof verifica se um objeto é instância de uma classe (ou de suas classes pai).
class Animal {}
class Cachorro extends Animal {}
class Gato extends Animal {}
const rex = new Cachorro();
const mimi = new Gato();
console.log(rex instanceof Cachorro); // true
console.log(rex instanceof Animal); // true (herdou!)
console.log(rex instanceof Gato); // false
console.log(mimi instanceof Animal); // true
Polimorfismo: Um Método, Múltiplas Formas
Polimorfismo é a capacidade de objetos de diferentes classes responderem ao mesmo método de maneiras diferentes. Já vimos isso com fazerSom()!
class Funcionario {
constructor(nome) {
this.nome = nome;
}
calcularSalario() {
return 0; // Genérico
}
}
class FuncionarioCLT extends Funcionario {
constructor(nome, salarioBase) {
super(nome);
this.salarioBase = salarioBase;
}
calcularSalario() {
return this.salarioBase - (this.salarioBase * 0.08); // Desconto INSS
}
}
class FuncionarioPJ extends Funcionario {
constructor(nome, valorHora, horas) {
super(nome);
this.valorHora = valorHora;
this.horas = horas;
}
calcularSalario() {
return this.valorHora * this.horas;
}
}
// Polimorfismo em ação!
const funcionarios = [
new FuncionarioCLT('João', 3000),
new FuncionarioPJ('Maria', 100, 160)
];
funcionarios.forEach(func => {
console.log(`${func.nome}: R$ ${func.calcularSalario()}`);
});
// "João: R$ 2760"
// "Maria: R$ 16000"
Classe Abstrata Simulada
JavaScript não tem classes abstratas nativas (como Java ou C#), mas podemos simulá-las. Uma classe abstrata não pode ser instanciada diretamente - ela serve apenas como modelo.
class Veiculo {
constructor(marca) {
if (new.target === Veiculo) {
throw new Error('Não pode instanciar Veiculo diretamente!');
}
this.marca = marca;
}
// Método "abstrato" - deve ser implementado pelas filhas
ligar() {
throw new Error('Método ligar() deve ser implementado!');
}
// Método concreto - já tem implementação
info() {
return `Veículo da marca ${this.marca}`;
}
}
class Carro extends Veiculo {
constructor(marca, modelo) {
super(marca);
this.modelo = modelo;
}
ligar() {
console.log(`Vrum vrum! ${this.marca} ${this.modelo} ligado!`);
}
}
// Isso vai dar erro:
// const veiculo = new Veiculo('Ford'); // Error!
// Isso funciona:
const fusca = new Carro('Volkswagen', 'Fusca');
fusca.ligar(); // "Vrum vrum! Volkswagen Fusca ligado!"
console.log(fusca.info()); // "Veículo da marca Volkswagen"
Exemplo Prático Completo
Vamos construir um sistema simples de uma biblioteca!
// Classe abstrata simulada
class ItemBiblioteca {
constructor(titulo, ano) {
if (new.target === ItemBiblioteca) {
throw new Error('ItemBiblioteca é abstrata!');
}
this.titulo = titulo;
this.ano = ano;
this.emprestado = false;
}
// Método abstrato
descricao() {
throw new Error('Método descricao() deve ser implementado!');
}
emprestar() {
if (this.emprestado) {
console.log(`${this.titulo} já está emprestado!`);
return false;
}
this.emprestado = true;
console.log(`${this.titulo} emprestado com sucesso!`);
return true;
}
devolver() {
if (!this.emprestado) {
console.log(`${this.titulo} não está emprestado!`);
return false;
}
this.emprestado = false;
console.log(`${this.titulo} devolvido com sucesso!`);
return true;
}
info() {
return `${this.titulo} (${this.ano}) - ${this.emprestado ? 'Emprestado' : 'Disponível'}`;
}
}
// Classe Livro
class Livro extends ItemBiblioteca {
constructor(titulo, ano, autor, paginas) {
super(titulo, ano);
this.autor = autor;
this.paginas = paginas;
}
descricao() {
return `Livro: ${this.titulo} por ${this.autor}, ${this.paginas} páginas`;
}
info() {
return `${super.info()} - Autor: ${this.autor}`;
}
}
// Classe DVD
class DVD extends ItemBiblioteca {
constructor(titulo, ano, diretor, duracao) {
super(titulo, ano);
this.diretor = diretor;
this.duracao = duracao;
}
descricao() {
return `DVD: ${this.titulo} dirigido por ${this.diretor}, ${this.duracao}min`;
}
info() {
return `${super.info()} - Diretor: ${this.diretor}`;
}
}
// Classe Revista (herda de ItemBiblioteca também)
class Revista extends ItemBiblioteca {
constructor(titulo, ano, edicao) {
super(titulo, ano);
this.edicao = edicao;
}
descricao() {
return `Revista: ${this.titulo}, Edição ${this.edicao}`;
}
}
// Testando o sistema
console.log('=== SISTEMA DA BIBLIOTECA ===\n');
const itens = [
new Livro('O Senhor dos Anéis', 1954, 'J.R.R. Tolkien', 1178),
new DVD('Matrix', 1999, 'Irmãos Wachowski', 136),
new Revista('Super Interessante', 2023, 250)
];
// Polimorfismo: cada item responde ao mesmo método de forma diferente
itens.forEach(item => {
console.log(item.descricao());
console.log(`Status: ${item.info()}\n`);
});
// Testando empréstimos
console.log('=== TESTE DE EMPRÉSTIMOS ===\n');
const livro = itens[0];
const dvd = itens[1];
livro.emprestar(); // "O Senhor dos Anéis emprestado com sucesso!"
livro.emprestar(); // "O Senhor dos Anéis já está emprestado!"
dvd.emprestar(); // "Matrix emprestado com sucesso!"
livro.devolver(); // "O Senhor dos Anéis devolvido com sucesso!"
dvd.devolver(); // "Matrix devolvido com sucesso!"
// Verificando tipos
console.log('\n=== VERIFICAÇÃO DE TIPOS ===');
console.log(livro instanceof Livro); // true
console.log(livro instanceof ItemBiblioteca); // true
console.log(livro instanceof DVD); // false
console.log(dvd instanceof ItemBiblioteca); // true
// Tentando instanciar classe abstrata
// const item = new ItemBiblioteca('Teste', 2024); // Error!
Dicas Importantes
- Sempre chame
super()antes de usarthisno construtor da classe filha - Use
instanceofpara verificar tipos, mas cuidado com heranças profundas - Prefira composição em vez de herança quando a relação não for "é um" (ex: um Carro "tem um" Motor, não "é um" Motor)
- Mantenha a hierarquia rasa - heranças muito profundas são difíceis de manter
Erros Comuns
// ❌ Erro: Esquecer super()
class Gato extends Animal {
constructor(nome) {
// Faltou super(nome);
this.nome = nome; // ReferenceError
}
}
// ❌ Erro: Chamar super() depois de usar this
class Gato extends Animal {
constructor(nome) {
this.vidas = 7; // ReferenceError
super(nome);
}
}
// ✅ Correto
class Gato extends Animal {
constructor(nome) {
super(nome);
this.vidas = 7;
}
}
// ❌ Erro: Achar que herança substitui composição
class Carro extends Motor {
// Um carro NÃO é um motor!
}
// ✅ Melhor: Composição
class Carro {
constructor() {
this.motor = new Motor();
}
}
Conclusão
Herança em JavaScript é poderosa quando usada corretamente. Lembre-se:
extendspara herdarsuper()para chamar o construtor/métodos do pai- Override para personalizar comportamentos
- Polimorfismo para tratar objetos diferentes de forma uniforme
- Classes abstratas simuladas para criar modelos
Pratique criando seus próprios sistemas - uma loja, uma escola, um jogo - e veja como a herança pode organizar seu código!
Bons estudos e até a próxima! 🚀