Prototypes: o que está por baixo
Nesta aula, exploramos o mecanismo de protótipos do JavaScript, entendendo como a herança funciona por baixo dos panos. Aprendemos a diferença entre __proto__ e prototype, como a cadeia de protótipos é formada e como as classes modernas utilizam esse mesmo sistema. Ao final, você terá uma compreensão sólida para debugar e otimizar código que depende de herança.
Os protótipos são o coração do JavaScript. Mesmo que você utilize classes, funções construtoras ou objetos literais, tudo se baseia em protótipos. Nesta aula, vamos desvendar esse mecanismo: o que é a cadeia de protótipos, como acessá-la, a diferença entre __proto__ e prototype, e como as classes modernas são apenas açúcar sintático sobre esse sistema. Prepare-se para olhar além da superfície e entender o que realmente acontece quando você cria objetos e herda propriedades.
Vamos começar com uma visão geral: todo objeto em JavaScript possui uma propriedade interna chamada [[Prototype]] (que aponta para outro objeto ou null). Quando você tenta acessar uma propriedade que não existe no objeto, o JavaScript a procura na cadeia de protótipos até encontrá-la ou chegar ao fim. Esse é o princípio da herança prototipal.
Cadeia de protótipos
A cadeia de protótipos é uma sequência de objetos ligados entre si por suas referências internas [[Prototype]]. Ela permite que objetos compartilhem propriedades e métodos de forma eficiente, sem duplicar código. Quando você acessa uma propriedade, o JavaScript percorre essa cadeia: primeiro no próprio objeto, depois no seu protótipo, depois no protótipo do protótipo, até chegar a Object.prototype (o topo) e, finalmente, null.
Vamos construir um exemplo prático. Criaremos um objeto animal com um método falar, e depois um objeto cachorro que herda de animal usando Object.create(). Observe como a propriedade falar é encontrada na cadeia:
const animal = {
falar() {
return 'Som genérico';
}
};
const cachorro = Object.create(animal);
cachorro.nome = 'Rex';
console.log(cachorro.falar()); // 'Som genérico' (herdado)
console.log(cachorro.hasOwnProperty('falar')); // false
console.log(cachorro.hasOwnProperty('nome')); // true
// Verificando a cadeia:
console.log(Object.getPrototypeOf(cachorro) === animal); // true
console.log(Object.getPrototypeOf(animal) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null
Perceba que cachorro não possui a propriedade falar diretamente, mas a encontra em seu protótipo (o objeto animal). Isso é possível graças à cadeia de protótipos. É importante entender que a cadeia é dinâmica: se você adicionar um método ao protótipo, todos os objetos que herdam dele o verão imediatamente.
__proto__ vs prototype
Dois termos frequentemente confundidos: __proto__ (duplo underscore) e prototype. Eles são diferentes, embora relacionados. __proto__ é um acessor (getter/setter) que expõe o protótipo interno de um objeto. Ele existe em quase todos os ambientes (embora seja considerado legado e não padronizado até recentemente). Já prototype é uma propriedade que só existe em funções construtoras (incluindo classes) e aponta para o objeto que será usado como protótipo dos objetos criados por essa função.
Vamos esclarecer com um exemplo. Quando você define uma função construtora Pessoa, ela tem uma propriedade prototype. Quando você cria uma instância com new Pessoa(), o protótipo interno dessa instância (acessado via __proto__ ou Object.getPrototypeOf()) aponta para Pessoa.prototype. Ou seja, pessoa1.__proto__ === Pessoa.prototype é verdadeiro.
function Pessoa(nome) {
this.nome = nome;
}
Pessoa.prototype.saudacao = function() {
return `Olá, meu nome é ${this.nome}`;
};
const p1 = new Pessoa('Ana');
console.log(p1.saudacao()); // 'Olá, meu nome é Ana'
console.log(p1.__proto__ === Pessoa.prototype); // true
console.log(Object.getPrototypeOf(p1) === Pessoa.prototype); // true (forma recomendada)
// A função também tem seu próprio __proto__, que aponta para Function.prototype
console.log(Pessoa.__proto__ === Function.prototype); // true
Uma confusão comum é achar que Pessoa.prototype é o protótipo da função Pessoa em si. Não! A função Pessoa é um objeto, e seu protótipo é Function.prototype. Pessoa.prototype é o protótipo que será atribuído às instâncias criadas por new Pessoa().
Recomenda-se usar Object.getPrototypeOf() e Object.setPrototypeOf() em vez de __proto__, pois são padronizados e funcionam em todos os ambientes. __proto__ é suportado por razões de compatibilidade, mas pode ser desabilitado em alguns contextos (como objetos com null prototype).
Como classes usam protótipos
As classes introduzidas no ES6 (ECMAScript 2015) são açúcar sintático sobre funções construtoras e protótipos. Por baixo dos panos, uma classe define uma função construtora e atribui métodos ao seu prototype. A sintaxe de classe torna o código mais limpo e legível, mas o mecanismo subjacente é o mesmo.
Vamos criar a mesma classe Pessoa usando a sintaxe de classe e verificar que o comportamento é idêntico:
class PessoaClass {
constructor(nome) {
this.nome = nome;
}
saudacao() {
return `Olá, meu nome é ${this.nome}`;
}
}
const p2 = new PessoaClass('Carlos');
console.log(p2.saudacao()); // 'Olá, meu nome é Carlos'
console.log(p2.__proto__ === PessoaClass.prototype); // true
console.log(typeof PessoaClass); // 'function' (é uma função)
Além disso, a herança entre classes (extends) também é implementada via cadeia de protótipos. Quando você usa class Cachorro extends Animal, o prototype de Cachorro é configurado para ter seu próprio __proto__ apontando para Animal.prototype. Assim, os métodos da classe pai são encontrados na cadeia.
class Animal {
falar() {
return 'Som genérico';
}
}
class Cachorro extends Animal {
falar() {
return 'Au au';
}
}
const c = new Cachorro();
console.log(c.falar()); // 'Au au' (sobrescreve)
console.log(c.__proto__ === Cachorro.prototype); // true
console.log(Cachorro.prototype.__proto__ === Animal.prototype); // true
Entender que classes são protótipos por baixo é crucial para depuração e para usar recursos como super (que internamente percorre a cadeia de protótipos) e para otimizar código que cria muitas instâncias, pois métodos no protótipo são compartilhados, economizando memória.
Boas práticas
Ao trabalhar com protótipos, prefira sempre a notação de classe para novos códigos, pois é mais clara e menos propensa a erros. Evite modificar protótipos de objetos nativos (como Array.prototype) para não causar conflitos em bibliotecas. Use Object.getPrototypeOf() e Object.setPrototypeOf() em vez de __proto__ para compatibilidade e segurança. Lembre-se de que a cadeia de protótipos é viva: alterações no protótipo afetam todas as instâncias, o que pode ser útil ou perigoso, dependendo do contexto.
Referências
- Herança e cadeia de protótipos (MDN)
- Object.getPrototypeOf (MDN)
- Object.setPrototypeOf (MDN)
- Classes (MDN)
- super (MDN)
- JavaScript Object Prototypes (W3Schools)
Exercícios
-
Crie um objeto
veiculocom uma propriedadetipoigual a 'genérico' e um métodoinfo()que retorna o tipo. Depois, crie um objetocarroque herda deveiculousandoObject.create()e adicione uma propriedademarca. Teste o acesso ainfo()e verifique a cadeia de protótipos.✓ Resposta:const veiculo = { tipo: 'genérico', info() { return `Tipo: ${this.tipo}`; } }; const carro = Object.create(veiculo); carro.marca = 'Toyota'; console.log(carro.info()); // 'Tipo: genérico' console.log(carro.tipo); // 'genérico' (herdado) console.log(Object.getPrototypeOf(carro) === veiculo); // true -
Explique a diferença entre
__proto__eprototypecom um exemplo de código que mostre ambos.✓ Resposta:__proto__é uma propriedade de acesso que existe em qualquer objeto e aponta para o protótipo interno desse objeto.prototypeé uma propriedade que só existe em funções construtoras (incluindo classes) e é o objeto que será usado como protótipo das instâncias criadas por essa função.function Construtor() {} const obj = new Construtor(); console.log(obj.__proto__ === Construtor.prototype); // true console.log(Construtor.prototype); // objeto vazio -
Escreva uma classe
Pessoacom um construtor que recebe nome e idade, e um métodoapresentar()que retorna uma string. Crie uma instância e verifique que o método está no protótipo e não no objeto.✓ Resposta:class Pessoa { constructor(nome, idade) { this.nome = nome; this.idade = idade; } apresentar() { return `Olá, sou ${this.nome} e tenho ${this.idade} anos.`; } } const p = new Pessoa('Maria', 30); console.log(p.apresentar()); console.log(p.hasOwnProperty('apresentar')); // false console.log(p.__proto__.hasOwnProperty('apresentar')); // true -
Considere o código abaixo. O que será impresso e por quê? Explique a cadeia de protótipos envolvida.
function Animal() {} Animal.prototype.falar = function() { return 'Som animal'; }; function Cachorro() {} Cachorro.prototype = Object.create(Animal.prototype); Cachorro.prototype.constructor = Cachorro; const c = new Cachorro(); console.log(c.falar()); console.log(c instanceof Animal); // true?✓ Resposta:Será impresso 'Som animal'. O objeto
cnão tem a propriedadefalar, mas a encontra na cadeia:c.__proto__éCachorro.prototype, que é um objeto criado comObject.create(Animal.prototype), então seu protótipo éAnimal.prototype, que possuifalar. Além disso,c instanceof AnimalétrueporqueAnimal.prototypeestá na cadeia de protótipos dec. -
Como você criaria uma cadeia de protótipos com três níveis:
aherda deb, ebherda dec? UseObject.create()e verifique cada nível comObject.getPrototypeOf().✓ Resposta:const c = { nivel: 'c' }; const b = Object.create(c); b.nivel = 'b'; const a = Object.create(b); a.nivel = 'a'; console.log(Object.getPrototypeOf(a) === b); // true console.log(Object.getPrototypeOf(b) === c); // true console.log(Object.getPrototypeOf(c) === Object.prototype); // true