Herança e polimorfismo
Nesta aula, exploramos os conceitos de herança e polimorfismo em JavaScript, fundamentais para a programação orientada a objetos. Aprendemos a usar as palavras-chave extends e super, como sobrescrever métodos, e como verificar a cadeia de protótipos com instanceof. Através de exemplos práticos e exercícios, você entenderá como criar hierarquias de classes e escrever código mais reutilizável e flexível.
Herança e polimorfismo são dois pilares da programação orientada a objetos que permitem criar código mais organizado, reutilizável e flexível. Em JavaScript, esses conceitos são implementados através de protótipos e, a partir do ES6, com a sintaxe de classes. Com a herança, uma classe pode herdar propriedades e métodos de outra, evitando duplicação e promovendo a reutilização. O polimorfismo, por sua vez, permite que objetos de diferentes classes respondam ao mesmo método de maneiras distintas, facilitando a criação de código que trabalha com abstrações.
Nesta aula, vamos mergulhar nos detalhes de como implementar herança em JavaScript moderno, explorando as palavras-chave extends e super, a sobrescrita de métodos, e o operador instanceof. Vamos entender não apenas a sintaxe, mas também o comportamento subjacente, para que você possa aplicar esses conceitos de forma eficaz em seus projetos.
extends
A palavra-chave extends é usada para criar uma classe que herda de outra classe (chamada de classe base ou superclasse). A nova classe, chamada de classe derivada ou subclasse, ganha acesso a todos os métodos e propriedades da classe base, podendo também adicionar novos membros ou modificar os existentes.
Em JavaScript, a herança é baseada em protótipos, mas a sintaxe extends torna esse processo mais claro e similar a outras linguagens orientadas a objetos. Quando uma classe estende outra, ela estabelece uma cadeia de protótipos: a subclasse tem como protótipo a superclasse, permitindo que os métodos sejam encontrados ao longo dessa cadeia.
Vejamos um exemplo básico:
class Animal {
constructor(nome) {
this.nome = nome;
}
falar() {
console.log(`${this.nome} faz um som.`);
}
}
class Cachorro extends Animal {
// A classe Cachorro herda o construtor e o método falar de Animal
}
const rex = new Cachorro('Rex');
rex.falar(); // "Rex faz um som."
No exemplo acima, Cachorro estende Animal sem adicionar nada novo. Portanto, instâncias de Cachorro têm acesso ao construtor e ao método falar da classe Animal. Isso demonstra a reutilização de código: não precisamos reescrever o método falar na classe Cachorro.
É importante notar que extends também funciona com classes que são expressões ou com funções construtoras tradicionais. No entanto, o uso mais comum é com a sintaxe de classes.
super
A palavra-chave super é usada para acessar e chamar funções do objeto pai (a superclasse). Existem dois usos principais: dentro do construtor da subclasse, para chamar o construtor da classe base; e em métodos, para chamar métodos da superclasse.
Quando você define um construtor em uma subclasse, você deve chamar super() antes de usar this. Isso é necessário para inicializar o objeto herdado corretamente. Se você não definir um construtor, o construtor padrão da superclasse é usado implicitamente.
Vamos ver um exemplo com construtor personalizado:
class Animal {
constructor(nome) {
this.nome = nome;
}
falar() {
console.log(`${this.nome} faz um som.`);
}
}
class Cachorro extends Animal {
constructor(nome, raca) {
super(nome); // chama o construtor da classe Animal
this.raca = raca;
}
// Podemos adicionar novos métodos
buscar() {
console.log(`${this.nome} está buscando a bola!`);
}
}
const rex = new Cachorro('Rex', 'Labrador');
console.log(rex.nome); // "Rex"
console.log(rex.raca); // "Labrador"
rex.falar(); // "Rex faz um som."
rex.buscar(); // "Rex está buscando a bola!"
No exemplo, super(nome) chama o construtor de Animal, passando o parâmetro nome. Isso define a propriedade this.nome na instância. Depois, adicionamos a propriedade raca específica da subclasse.
Além do construtor, super também pode ser usado em métodos para invocar métodos da superclasse. Isso é útil quando queremos estender o comportamento de um método existente:
class Animal {
constructor(nome) {
this.nome = nome;
}
falar() {
console.log(`${this.nome} faz um som.`);
}
}
class Cachorro extends Animal {
falar() {
super.falar(); // chama o método falar da classe Animal
console.log(`${this.nome} late.`);
}
}
const rex = new Cachorro('Rex');
rex.falar();
// "Rex faz um som."
// "Rex late."
Aqui, a classe Cachorro sobrescreve o método falar e usa super.falar() para reutilizar a lógica da superclasse antes de adicionar seu próprio comportamento. Isso é uma forma de polimorfismo, pois a chamada ao método falar em uma instância de Cachorro executa a versão sobrescrita.
Sobrescrita de métodos
Sobrescrever um método significa definir um novo comportamento para um método que já existe na superclasse. Isso é fundamental para o polimorfismo, pois permite que uma subclasse forneça uma implementação específica de um método que é comum a várias classes.
Para sobrescrever um método, basta declarar um método com o mesmo nome na subclasse. A versão da subclasse será usada em vez da versão da superclasse quando o método for chamado em uma instância da subclasse. Você pode, se desejar, chamar a versão da superclasse usando super.metodo().
Vamos considerar um exemplo mais elaborado:
class Forma {
constructor() {}
area() {
return 0;
}
descricao() {
return 'Esta é uma forma genérica.';
}
}
class Circulo extends Forma {
constructor(raio) {
super();
this.raio = raio;
}
area() {
return Math.PI * this.raio ** 2;
}
descricao() {
return `Este é um círculo com raio ${this.raio}.`;
}
}
class Retangulo extends Forma {
constructor(largura, altura) {
super();
this.largura = largura;
this.altura = altura;
}
area() {
return this.largura * this.altura;
}
descricao() {
return `Este é um retângulo com largura ${this.largura} e altura ${this.altura}.`;
}
}
const formas = [new Circulo(5), new Retangulo(4, 6)];
formas.forEach(forma => {
console.log(forma.descricao());
console.log(`Área: ${forma.area()}`);
});
Neste exemplo, temos uma classe base Forma com métodos area() e descricao(). As subclasses Circulo e Retangulo sobrescrevem ambos os métodos para fornecer implementações específicas. Quando iteramos sobre o array de formas e chamamos descricao() e area(), o JavaScript resolve dinamicamente qual método executar com base no tipo real do objeto. Isso é polimorfismo em ação: o mesmo código funciona para diferentes tipos de objetos.
É possível também sobrescrever métodos estáticos, mas isso é menos comum. A sobrescrita de métodos é uma técnica poderosa para criar hierarquias de classes flexíveis e expressivas.
instanceof
O operador instanceof verifica se um objeto é uma instância de uma determinada classe (ou de uma classe que herda dela). A sintaxe é: objeto instanceof Classe. Ele retorna true se o objeto estiver na cadeia de protótipos da classe.
Esse operador é útil para verificar o tipo de um objeto em tempo de execução, especialmente quando você trabalha com hierarquias de classes e deseja tomar decisões baseadas no tipo.
Vejamos um exemplo:
class Animal {}
class Cachorro extends Animal {}
class Gato extends Animal {}
const rex = new Cachorro();
const mingau = new Gato();
console.log(rex instanceof Cachorro); // true
console.log(rex instanceof Animal); // true (pois Cachorro herda de Animal)
console.log(rex instanceof Gato); // false
console.log(mingau instanceof Animal); // true
No exemplo, rex é uma instância de Cachorro e também de Animal porque Cachorro estende Animal. Já mingau é uma instância de Gato e de Animal, mas não de Cachorro.
O instanceof também funciona com classes que não são declaradas com class, como funções construtoras tradicionais, e até mesmo com objetos que têm um Symbol.hasInstance personalizado. No entanto, na maioria dos casos, você usará com classes.
Uma observação importante: instanceof verifica a cadeia de protótipos. Se você criar um objeto com Object.create(AlgumaClasse.prototype), ele também será considerado uma instância de AlgumaClasse (desde que a cadeia seja mantida). Mas isso é um detalhe avançado.
Boas práticas e observações finais
Ao trabalhar com herança, é importante lembrar que a composição muitas vezes é preferível à herança (o princípio de 'prefira composição em vez de herança'). A herança pode levar a hierarquias rígidas e problemas de acoplamento. Avalie se a relação é realmente 'é um' antes de usar herança.
Além disso, evite heranças profundas (mais de alguns níveis), pois isso dificulta a manutenção e a compreensão do código. Prefira métodos pequenos e bem definidos, e use sobrescrita com cuidado para não quebrar o contrato da superclasse.
Por fim, lembre-se de que super() deve ser chamado antes de acessar this no construtor de uma subclasse. Se você esquecer, um erro será lançado.
Exercícios
-
Crie uma classe
Veiculocom propriedadesmarcaeano, e um métodoinfo()que retorna uma string como "Marca: [marca], Ano: [ano]". Em seguida, crie uma classeCarroque estendaVeiculo, adicione a propriedadeportase sobrescreva o métodoinfo()para incluir o número de portas. Teste criando uma instância deCarroe chamandoinfo().✓ Resposta:class Veiculo { constructor(marca, ano) { this.marca = marca; this.ano = ano; } info() { return `Marca: ${this.marca}, Ano: ${this.ano}`; } } class Carro extends Veiculo { constructor(marca, ano, portas) { super(marca, ano); this.portas = portas; } info() { return `${super.info()}, Portas: ${this.portas}`; } } const meuCarro = new Carro('Toyota', 2020, 4); console.log(meuCarro.info()); // "Marca: Toyota, Ano: 2020, Portas: 4" -
Explique o que acontece se você tentar acessar
thisem um construtor de uma subclasse antes de chamarsuper(). Forneça um exemplo que cause erro.✓ Resposta:class Base { constructor() {} } class Derivada extends Base { constructor() { // this.nome = 'teste'; // Isso causa erro: Must call super constructor in derived class before accessing 'this' super(); this.nome = 'teste'; } } const d = new Derivada(); -
Dado o seguinte código:
Qual é a saída? Explique o papel declass A { metodo() { console.log('A'); } } class B extends A { metodo() { console.log('B'); super.metodo(); } } const b = new B(); b.metodo();super.✓ Resposta:// Saída: // B // A // O método metodo() em B sobrescreve o de A. Dentro dele, primeiro imprime 'B', depois chama super.metodo() que executa o método da classe A, imprimindo 'A'. -
Crie uma hierarquia de classes para formas geométricas:
Forma(classe base),CirculoeQuadrado. Cada uma deve ter um métodocalcularArea()que retorna a área. Use polimorfismo para criar uma função que recebe um array de formas e imprime a área de cada uma.✓ Resposta:class Forma { calcularArea() { return 0; } } class Circulo extends Forma { constructor(raio) { super(); this.raio = raio; } calcularArea() { return Math.PI * this.raio ** 2; } } class Quadrado extends Forma { constructor(lado) { super(); this.lado = lado; } calcularArea() { return this.lado * this.lado; } } function imprimirAreas(formas) { formas.forEach(forma => { console.log(forma.calcularArea()); }); } const formas = [new Circulo(2), new Quadrado(3), new Circulo(1)]; imprimirAreas(formas); // 12.566..., 9, 3.141... -
Escreva um código que demonstre o uso de
instanceofpara verificar se um objeto é uma instância de uma classe específica, mesmo quando ele é criado a partir de uma subclasse. Use as classesAnimal,CachorroeGato.✓ Resposta:class Animal {} class Cachorro extends Animal {} class Gato extends Animal {} const rex = new Cachorro(); const mingau = new Gato(); console.log(rex instanceof Animal); // true console.log(rex instanceof Cachorro); // true console.log(rex instanceof Gato); // false console.log(mingau instanceof Animal); // true