Explorando Padrões de Herança em JavaScript

A herança em JavaScript pode ser implementada de diversas formas, cada uma com suas particularidades e desafios. Vamos analisar alguns dos padrões mais comuns.

1. Herança via Cadeia de Protótipos

Este método consiste em definir o protótipo de um tipo filho como uma instância de um tipo pai. A sintaxe chave é: TipoFilho.prototype = new TipoPai();.


// Construtor pai
function Veiculo(marca) {
   this.marca = marca;
   this.rodas = ['pneu dianteiro', 'pneu traseiro'];
}
// Métodos do protótipo pai
Veiculo.prototype.obterMarca = function() {
   console.log(this.marca);
}

// Construtor filho
function Carro(marca, modelo) {
   this.modelo = modelo;
}
// Estabelecendo a cadeia de protótipos
Carro.prototype = new Veiculo();

// Métodos do protótipo filho (adicionados após o estabelecimento da cadeia)
Carro.prototype.obterModelo = function() {
   console.log(this.modelo);
}

var meuCarro = new Carro('Toyota', 'Corolla');
console.log(meuCarro.rodas); // Saída: ['pneu dianteiro', 'pneu traseiro']
meuCarro.obterMarca();      // Saída: Toyota
meuCarro.obterModelo();      // Saída: Corolla

var outroCarro = new Carro();
outroCarro.rodas.push('pneu estepe');
console.log(meuCarro.rodas); // Saída: ['pneu dianteiro', 'pneu traseiro', 'pneu estepe']
 

As desvantagens deste padrão são:

  • Propriedades de referência (como arrays) são compartilhadas entre todas as instâncias, levando a efeitos colaterais indesejados.
  • Não é possível passar argumentos para o construtor pai ao criar instâncias do tipo filho sem afetar todas as outras instâncias.

Devido a essas limitações, a herança via cadeia de protótipos raramente é utilizada isoladamente na prática.

2. Herança por Empréstimo de Construtores (Constructor Stealing)

Para superar as limitações anteriores, este método invoca o construtor do tipo pai dentro do construtor do tipo filho, utilizando apply() ou call().


function Veiculo(marca) {
   this.marca = marca;
   this.rodas = ['pneu dianteiro', 'pneu traseiro'];
   this.ligarMotor = function() {
       console.log('Motor ligado!');
   }
}
Veiculo.prototype.obterMarca = function() {
   console.log(this.marca);
}

function Carro(marca, modelo) {
   // Empréstimo do construtor pai
   Veiculo.apply(this, [marca]);
   this.modelo = modelo;
}

var meuCarro = new Carro('Ford', 'Focus');
console.log(meuCarro.rodas); // Saída: ['pneu dianteiro', 'pneu traseiro']
meuCarro.ligarMotor();      // Saída: Motor ligado!
// meuCarro.obterMarca();   // Erro! obterMarca está no protótipo de Veiculo

var outroCarro = new Carro('Chevrolet', 'Onix');
outroCarro.rodas.push('pneu reserva');
console.log(meuCarro.rodas); // Saída: ['pneu dianteiro', 'pneu traseiro']
 

A principal desvantagem é que os métodos definidos no protótipo do tipo pai (como obterMarca) não são acessíveis às instâncias do tipo filho. A reutilização de código de métodos fica comprometida.

3. Herança Combinada

Este padrão une as duas abordagens anteriores: usa a cadeia de protótipos para herdar métodos e o empréstimo de construtores para herdar propriedades da instância.

A combinação envolve Carro.prototype = new Veiculo(); e Veiculo.apply(this, [marca]);. No entanto, isso resulta na chamada do construtor pai duas vezes: uma implicitamente ao criar a instância para o protótipo e outra explicitamente dentro do construtor filho.


function Veiculo(marca) {
   this.marca = marca;
   this.rodas = ['pneu dianteiro', 'pneu traseiro'];
   this.ligarMotor = function() {
       console.log('Motor ligado!');
   }
}
Veiculo.prototype.obterMarca = function() {
   console.log(this.marca);
}

function Carro(marca, modelo) {
   Veiculo.apply(this, [marca]); // Primeira chamada ao construtor pai
   this.modelo = modelo;
}
Carro.prototype = new Veiculo(); // Segunda chamada ao construtor pai (implícita)

var meuCarro = new Carro('Fiat', 'Palio');
console.log(meuCarro.rodas);      // Saída: ['pneu dianteiro', 'pneu traseiro']
meuCarro.ligarMotor();           // Saída: Motor ligad!
// meuCarro.obterMarca();        // Erro! Ainda não acessível

var outroCarro = new Carro('Honda', 'Civic');
outroCarro.rodas.push('pneu sobressalente');
console.log(meuCarro.rodas);      // Saída: ['pneu dianteiro', 'pneu traseiro']
 

A duplicação da chamada ao construtor pai pode ser problemática, especialmente com propriedades de referência.

4. Herança Prototípica (Object.create)

Este método, introduzido em ECMAScript 5 através de Object.create(), cria um novo objeto cujo protótipo é o objeto fornecido. Essencialmente, ele estabelece uma cadeia de protótipos sem a necessidade de invocar construtores diretamente para a herança prototípica.


var veiculoBase = {
   nome: 'Veículo Genérico',
   rodas: ['pneu dianteiro', 'pneu traseiro'],
   obterNome: function() {
       console.log(this.nome);
   }
};

// Criando um novo objeto com veiculoBase como seu protótipo
var meuCarro = Object.create(veiculoBase);
meuCarro.nome = 'Meu Carro';
meuCarro.rodas.push('pneu estepe');
console.log(meuCarro.rodas); // Saída: ['pneu dianteiro', 'pneu traseiro', 'pneu estepe']
meuCarro.obterNome();        // Saída: Meu Carro

var outroCarro = Object.create(veiculoBase);
console.log(outroCarro.rodas); // Saída: ['pneu dianteiro', 'pneu traseiro', 'pneu estepe'] (compartilhado)
 

Assim como na herança via cadeia de protótipos, propriedades de referência em objetos compartilhados no protótipo podem levar a comportamentos inesperados.

5. Herança Prototípica com Empréstimo de Construtores (Parasitic Combination Inheritance)

Considerado o padrão mais robusto, ele combina o melhor de duas abordagens: usa o empréstimo de construtores para herdar propriedades da instância e uma forma modificada de herança prototípica para herdar métodos do protótipo, evitando a duplicação da chamada ao construtor pai.

A ideia central é criar uma cópia do protótipo do tipo pai sem executar seu construtor, e então atribuir essa cópia ao protótipo do tipo filho.


// Função auxiliar para estabelecer a herança prototípica
function herdarProtótipo(subTipo, superTipo) {
   var protótipo = Object.create(superTipo.prototype); // Cria um objeto com o protótipo do superTipo
   protótipo.constructor = subTipo;                   // Corrige o construtor
   subTipo.prototype = protótipo;                     // Define o novo protótipo
}

// Construtor pai
function Veiculo(marca) {
   this.marca = marca;
   this.rodas = ['pneu dianteiro', 'pneu traseiro'];
}
Veiculo.prototype.obterMarca = function() {
   console.log(this.marca);
}

// Construtor filho
function Carro(marca, modelo) {
   Veiculo.call(this, marca); // Empréstimo de construtor para propriedades da instância
   this.modelo = modelo;
}

// Estabelece a herança prototípica
herdarProtótipo(Carro, Veiculo);

var meuCarro = new Carro('Volkswagen', 'Gol');
meuCarro.rodas.push('pneu sobressalente');
console.log(meuCarro.rodas); // Saída: ['pneu dianteiro', 'pneu traseiro', 'pneu sobressalente']

var outroCarro = new Carro('Renault', 'Sandero');
console.log(outroCarro.rodas); // Saída: ['pneu dianteiro', 'pneu traseiro'] (não compartilhado)
outroCarro.obterMarca();      // Saída: Renault
 

Este padrão resolve as principais desvantagens dos métodos anteriores, proporcionando uma herança mais controlada e eficiente.

Tags: javascript Herança protótipos object.create construtores

Publicado em 6-9 00:32 por Thomas