Entendendo o Prototype Chain
O prototype chain é o mecanismo fundamental de herança em JavaScript. Quando você acessa uma propriedade em um objeto, o JavaScript primeiro procura naquele objeto. Se não encontrar, busca no prototype do objeto, depois no prototype do prototype, e assim sucessivamente até chegar a null. Essa cadeia de prototypes é o que chamamos de prototype chain.
Diferentemente de linguagens baseadas em classes, JavaScript usa herança baseada em protótipos onde objetos herdam diretamente de outros objetos. Entender isso é crucial para escrever código eficiente e evitar armadilhas comuns em produção. Vamos explorar como isso funciona na prática.
const animal = {
som: function() {
console.log('Som genérico');
}
};
const cachorro = Object.create(animal);
cachorro.latir = function() {
console.log('Au au!');
};
cachorro.som(); // 'Som genérico' - herdado de animal
cachorro.latir(); // 'Au au!' - próprio do objeto
Construtores e o Operador new
Historicamente, JavaScript usava funções construtoras para criar objetos com prototypes. Quando você chama uma função com new, o JavaScript cria um novo objeto, define seu [[Prototype]] como o prototype da função construtora, e executa a função naquele contexto. Essa abordagem ainda é amplamente usada em código legado e é essencial compreender.
A propriedade prototype de uma função construtora define o que será o [[Prototype]] de todos os objetos criados por ela. Note a diferença: [[Prototype]] é uma propriedade interna (acessível via Object.getPrototypeOf()), enquanto prototype é uma propriedade regular da função.
function Veiculo(marca, velocidade) {
this.marca = marca;
this.velocidade = velocidade;
}
Veiculo.prototype.acelerar = function() {
this.velocidade += 10;
return `${this.marca} acelerou para ${this.velocidade}km/h`;
};
const carro = new Veiculo('Toyota', 60);
console.log(carro.acelerar()); // 'Toyota acelerou para 70km/h'
console.log(Object.getPrototypeOf(carro) === Veiculo.prototype); // true
Evitando Armadilhas com Prototypes
Um erro comum é modificar Veiculo.prototype depois de criar instâncias ou compartilhar arrays/objetos entre instâncias. Se você quer propriedades únicas por instância, defina-as no construtor, não no prototype. Propriedades compartilhadas (métodos) vão no prototype.
function Usuario(nome) {
this.nome = nome;
this.amigos = []; // Correto: cada instância tem seu próprio array
}
Usuario.prototype.adicionarAmigo = function(amigo) {
this.amigos.push(amigo); // Correto: método no prototype
};
const alice = new Usuario('Alice');
const bob = new Usuario('Bob');
alice.adicionarAmigo('Charlie');
console.log(bob.amigos); // [] - Bob não foi afetado
Herança em Produção com Object.create() e Classes
Em produção moderna, preferimos Object.create() ou classes ES6. O Object.create() oferece controle explícito sobre o prototype, tornando o código mais legível. Já as classes ES6 são açúcar sintático sobre construtores, mas facilitam a escrita e compreensão da herança.
// Padrão com Object.create() - explícito e claro
const Animal = {
fazer_som: function() {
console.log('Som');
}
};
const Gato = Object.create(Animal);
Gato.miar = function() {
console.log('Miau!');
};
const meuGato = Object.create(Gato);
meuGato.nome = 'Felix';
meuGato.miar(); // 'Miau!'
meuGato.fazer_som(); // 'Som'
Para hierarquias mais complexas, classes ES6 são superiores. Elas compilam para o mesmo prototype chain, mas com sintaxe muito mais clara. Use extends para herança e super para acessar o prototype pai.
class Pessoa {
constructor(nome, idade) {
this.nome = nome;
this.idade = idade;
}
apresentar() {
return `Olá, meu nome é ${this.nome}`;
}
}
class Desenvolvedor extends Pessoa {
constructor(nome, idade, linguagem) {
super(nome, idade);
this.linguagem = linguagem;
}
apresentar() {
return `${super.apresentar()} e programo em ${this.linguagem}`;
}
}
const dev = new Desenvolvedor('Maria', 28, 'JavaScript');
console.log(dev.apresentar());
// 'Olá, meu nome é Maria e programo em JavaScript'
Boas Práticas em Produção
Em produção, sempre use classes ES6 para código novo. Elas são mais seguras, legíveis e os transpiladores as suportam completamente. Evite modificar prototypes de objetos globais como Array.prototype ou Object.prototype. Use Object.freeze() em prototypes críticos para evitar alterações acidentais. Sempre prefira composição quando a hierarquia fica profunda (mais de 3 níveis).
// ❌ Ruim: modificar prototype global
Array.prototype.meuMetodo = function() {};
// ✅ Bom: estender com uma classe específica
class MinhaLista extends Array {
meuMetodo() {
// implementação
}
}
// ✅ Bom: usar composição em vez de herança profunda
class ServidorWeb {
constructor(banco, autenticador, logger) {
this.banco = banco;
this.autenticador = autenticador;
this.logger = logger;
}
}
Conclusão
O prototype chain é o coração de JavaScript e dominar sua herança baseada em protótipos é essencial para código profissional. Os três pontos principais que deve reter: 1) O prototype chain funciona por delegação — objetos herdam buscando propriedades em seus prototypes sucessivamente; 2) Use classes ES6 em produção — são mais seguras, legíveis e eliminam armadilhas; 3) Prefira composição para hierarquias complexas — evita acoplamento excessivo e torna o código mais flexível e testável.