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

Exercícios

  1. Crie um objeto veiculo com uma propriedade tipo igual a 'genérico' e um método info() que retorna o tipo. Depois, crie um objeto carro que herda de veiculo usando Object.create() e adicione uma propriedade marca. Teste o acesso a info() 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
    
  2. Explique a diferença entre __proto__ e prototype com 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
    
  3. Escreva uma classe Pessoa com um construtor que recebe nome e idade, e um método apresentar() 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
    
  4. 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 c não tem a propriedade falar, mas a encontra na cadeia: c.__proto__ é Cachorro.prototype, que é um objeto criado com Object.create(Animal.prototype), então seu protótipo é Animal.prototype, que possui falar. Além disso, c instanceof Animal é true porque Animal.prototype está na cadeia de protótipos de c.

  5. Como você criaria uma cadeia de protótipos com três níveis: a herda de b, e b herda de c? Use Object.create() e verifique cada nível com Object.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