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

  1. Crie um objeto pessoa com propriedades nome e idade. Depois, crie um objeto aluno que herde de pessoa e adicione uma propriedade curso. Verifique se aluno possui as propriedades herdadas e próprias usando hasOwnProperty().

    ✓ 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')); // true
    
  2. Explique a diferença entre __proto__ e prototype e 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')); // true
    
  3. Usando uma classe ES6, crie uma classe Veiculo com propriedade marca e método mover(). Depois crie uma subclasse Carro que adicione a propriedade portas e sobrescreva o método mover(). Instancie um objeto e verifique a cadeia de protótipos com Object.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); // true
    
  4. Dado o código: function A() {} e function B() {}, faça B herdar de A usando Object.setPrototypeOf() (ou B.prototype = Object.create(A.prototype)). Instancie um objeto de B e confirme que ele herda de A.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); // true
    
  5. Escreva uma função que percorra a cadeia de protótipos de um objeto e imprima todos os protótipos até chegar a null (ou Object.prototype). Use Object.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)
    

Referências