Prototypes: o que está por baixo
Esta aula explora o conceito de protótipos em JavaScript, revelando como objetos herdam propriedades e métodos através da cadeia de protótipos. Você aprenderá a diferença entre __proto__ e prototype, e como as classes modernas utilizam protótipos internamente, com exemplos práticos e exercícios.
JavaScript é uma linguagem baseada em protótipos, não em classes tradicionais. Isso significa que a herança é implementada através de objetos que servem de modelo para outros objetos. Quando você acessa uma propriedade em um objeto, o JavaScript primeiro procura no próprio objeto e, se não encontrar, segue a cadeia de protótipos até encontrar a propriedade ou chegar ao fim da cadeia. Compreender essa mecânica é fundamental para dominar a linguagem, pois afeta como escrevemos código, como as classes funcionam e como podemos criar abstrações eficientes.
Nesta aula, vamos desvendar o que está por baixo dos protótipos: a cadeia de protótipos, a diferença entre as propriedades __proto__ e prototype, e como as classes ES6 são construídas sobre esse mecanismo. Ao final, você terá uma visão clara de como a herança funciona em JavaScript e estará preparado para lidar com conceitos avançados como herança prototípica e composição.
Cadeia de protótipos
A cadeia de protótipos é o mecanismo pelo qual objetos herdam propriedades e métodos de outros objetos. Todo objeto em JavaScript possui um protótipo interno (acessível via Object.getPrototypeOf() ou __proto__), que é outro objeto. Quando você tenta acessar uma propriedade, o JavaScript procura no objeto atual e, se não achar, procura no protótipo, depois no protótipo do protótipo, e assim por diante, até chegar a Object.prototype, que é o topo da cadeia. Se ainda não encontrar, retorna undefined.
Vamos criar um exemplo simples para ilustrar:
const animal = {
tipo: 'animal',
respirar() {
console.log('Respirando...');
}
};
const cachorro = Object.create(animal);
cachorro.nome = 'Rex';
console.log(cachorro.nome); // 'Rex' (própria)
console.log(cachorro.tipo); // 'animal' (herdada)
cachorro.respirar(); // 'Respirando...' (herdado)
console.log(Object.getPrototypeOf(cachorro) === animal); // true
Quando acessamos cachorro.tipo, o JavaScript não encontra a propriedade no próprio objeto, então sobe para o protótipo (animal) e a encontra. O mesmo ocorre com o método respirar. A cadeia termina em Object.prototype, que possui métodos como toString() e hasOwnProperty().
É importante notar que a cadeia é dinâmica: se você adicionar uma propriedade ao protótipo depois, todos os objetos que herdam dele a verão imediatamente. Porém, se você definir uma propriedade com o mesmo nome no objeto, ela irá sobrescrever a do protótipo (shadowing).
Para verificar se uma propriedade é própria ou herdada, use hasOwnProperty():
console.log(cachorro.hasOwnProperty('nome')); // true
console.log(cachorro.hasOwnProperty('tipo')); // false
A cadeia de protótipos é a base de toda herança em JavaScript. Entendê-la permite criar hierarquias de objetos de forma eficiente e evitar armadilhas comuns.
__proto__ vs prototype
Dois termos frequentemente confundidos são __proto__ e prototype. Eles são conceitos relacionados, mas distintos.
__proto__ é uma propriedade acessora (getter/setter) que expõe o protótipo interno de um objeto. É uma forma antiga e não padronizada, mas suportada por todos os navegadores. Você pode usá-lo para ler ou definir o protótipo de um objeto existente. Porém, é recomendado usar Object.getPrototypeOf() e Object.setPrototypeOf() para operações formais.
Por outro lado, prototype é uma propriedade que existe apenas em funções (quando usadas como construtoras). Ela define o objeto que será usado como protótipo para os objetos criados por essa função via new. Ou seja, funções têm a propriedade prototype, e os objetos criados por elas têm __proto__ apontando para esse objeto.
Vamos ver um exemplo clássico com uma função construtora:
function Pessoa(nome) {
this.nome = nome;
}
Pessoa.prototype.dizerOi = function() {
console.log('Olá, meu nome é ' + this.nome);
};
const joao = new Pessoa('João');
console.log(joao.__proto__ === Pessoa.prototype); // true
console.log(Object.getPrototypeOf(joao) === Pessoa.prototype); // true
joao.dizerOi(); // 'Olá, meu nome é João'
Aqui, Pessoa.prototype é um objeto que contém métodos compartilhados. Quando criamos joao, o JavaScript define o protótipo interno de joao como Pessoa.prototype (acessível via joao.__proto__). Note que Pessoa.prototype é um objeto normal, e seu próprio protótipo é Object.prototype.
Resumindo: __proto__ é a referência ao protótipo de um objeto específico, enquanto prototype é uma propriedade de funções construtoras que define o protótipo para os objetos criados por elas. Nunca confunda: Pessoa.prototype não é o protótipo da função Pessoa, mas sim o protótipo dos objetos criados por ela.
Para verificar a cadeia, podemos usar Object.getPrototypeOf():
console.log(Object.getPrototypeOf(joao) === Pessoa.prototype); // true
console.log(Object.getPrototypeOf(Pessoa.prototype) === Object.prototype); // true
Essa distinção é crucial para entender como herança funciona e como o JavaScript cria objetos.
Como classes usam protótipos
As classes ES6 (introduzidas em 2015) são açúcar sintático sobre o sistema de protótipos existente. Elas não introduzem um novo modelo de herança, mas oferecem uma sintaxe mais clara e familiar para programadores que vêm de linguagens baseadas em classes. Internamente, uma classe é uma função construtora com um protótipo.
Vamos definir uma classe e examinar sua estrutura:
class Animal {
constructor(nome) {
this.nome = nome;
}
falar() {
console.log(this.nome + ' faz um som.');
}
}
class Cachorro extends Animal {
falar() {
console.log(this.nome + ' late.');
}
}
const rex = new Cachorro('Rex');
rex.falar(); // 'Rex late.'
// Internamente:
console.log(typeof Cachorro); // 'function'
console.log(Cachorro.prototype === Object.getPrototypeOf(rex)); // true
console.log(Object.getPrototypeOf(Cachorro.prototype) === Animal.prototype); // true
Quando você declara uma classe, o JavaScript cria uma função com o nome da classe e define prototype com um objeto que contém os métodos da classe. O construtor é a própria função. A herança com extends estabelece a cadeia de protótipos: Cachorro.prototype tem como protótipo Animal.prototype, e Cachorro tem como protótipo Animal (isso é usado para herança estática).
Podemos verificar que métodos são compartilhados via protótipo:
console.log(rex.hasOwnProperty('nome')); // true
console.log(rex.hasOwnProperty('falar')); // false
console.log(Cachorro.prototype.hasOwnProperty('falar')); // true
As classes também suportam métodos estáticos, que são definidos na própria função construtora (não no protótipo). E os getters/setters também são definidos no protótipo.
Um detalhe importante: as classes são estritas por padrão ("use strict") e não podem ser chamadas sem new. Elas também não sofrem hoisting como funções.
Entender que classes são protótipos por baixo ajuda a depurar e a escrever código mais eficiente, além de facilitar a transição para padrões mais avançados, como mixins e composição.
Boas práticas e observações finais
Ao trabalhar com protótipos, prefira sempre Object.create(), Object.getPrototypeOf() e Object.setPrototypeOf() em vez de manipular __proto__ diretamente. Use classes para código novo, mas entenda que elas são apenas uma fachada para o sistema de protótipos. Evite modificar protótipos de objetos nativos (como Array.prototype) em código de produção, pois isso pode causar conflitos e quebrar bibliotecas. Sempre que possível, use herança de forma consciente e prefira composição se a hierarquia for complexa.
Outra dica: ao criar objetos com Object.create(), você pode controlar exatamente o protótipo, o que é útil para herança diferencial. Lembre-se de que a cadeia de protótipos pode ter impacto no desempenho, então evite cadeias muito longas e prefira propriedades próprias quando forem mais adequadas.
Exercícios
Crie um objeto
pessoacom propriedadesnomeeidade. Depois, crie um objetoalunoque herde depessoae adicione uma propriedadecurso. Verifique sealunopossui as propriedades herdadas e próprias usandohasOwnProperty().✓ Resposta:const pessoa = { nome: 'Ana', idade: 30 }; const aluno = Object.create(pessoa); aluno.curso = 'Engenharia'; console.log(aluno.nome); // 'Ana' (herdada) console.log(aluno.curso); // 'Engenharia' (própria) console.log(aluno.hasOwnProperty('nome')); // false console.log(aluno.hasOwnProperty('curso')); // trueExplique a diferença entre
__proto__eprototypee dê um exemplo onde cada um é usado corretamente.✓ Resposta:function Pessoa(nome) { this.nome = nome; } Pessoa.prototype.saudar = function() { console.log('Oi!'); }; const p = new Pessoa('Maria'); console.log(p.__proto__ === Pessoa.prototype); // true console.log(Pessoa.prototype); // objeto com saudar console.log(p.hasOwnProperty('nome')); // trueUsando uma classe ES6, crie uma classe
Veiculocom propriedademarcae métodomover(). Depois crie uma subclasseCarroque adicione a propriedadeportase sobrescreva o métodomover(). Instancie um objeto e verifique a cadeia de protótipos comObject.getPrototypeOf().✓ Resposta:class Veiculo { constructor(marca) { this.marca = marca; } mover() { console.log('Veículo se movendo'); } } class Carro extends Veiculo { constructor(marca, portas) { super(marca); this.portas = portas; } mover() { console.log('Carro dirigindo'); } } const fusca = new Carro('VW', 2); console.log(fusca.marca); // 'VW' console.log(fusca.portas); // 2 fusca.mover(); // 'Carro dirigindo' console.log(Object.getPrototypeOf(fusca) === Carro.prototype); // true console.log(Object.getPrototypeOf(Carro.prototype) === Veiculo.prototype); // trueDado o código:
function A() {}efunction B() {}, façaBherdar deAusandoObject.setPrototypeOf()(ouB.prototype = Object.create(A.prototype)). Instancie um objeto deBe confirme que ele herda deA.prototype.✓ Resposta:function A() {} A.prototype.saudacao = function() { return 'Olá'; }; function B() {} B.prototype = Object.create(A.prototype); B.prototype.constructor = B; const obj = new B(); console.log(obj.saudacao()); // 'Olá' console.log(Object.getPrototypeOf(obj) === B.prototype); // true console.log(Object.getPrototypeOf(B.prototype) === A.prototype); // trueEscreva uma função que percorra a cadeia de protótipos de um objeto e imprima todos os protótipos até chegar a
null(ouObject.prototype). UseObject.getPrototypeOf().✓ Resposta:function percorrerCadeia(obj) { let proto = Object.getPrototypeOf(obj); while (proto) { console.log(proto); proto = Object.getPrototypeOf(proto); } } const o = {}; percorrerCadeia(o); // imprime Object.prototype e depois null (para)