Prototype e cadeia de protótipos

1. Fundamentos do Prototype em JavaScript

Em JavaScript, prototype é o mecanismo fundamental pelo qual objetos herdam propriedades e métodos de outros objetos. Diferentemente de linguagens baseadas em classes, JavaScript utiliza herança prototípica — cada objeto possui uma referência interna para outro objeto chamado de prototype.

Todo objeto em JavaScript possui um prototype oculto, acessível via Object.getPrototypeOf() ou pela propriedade __proto__ (depreciada, mas amplamente suportada):

const pessoa = { nome: 'Ana' };
console.log(Object.getPrototypeOf(pessoa)); // Object.prototype
console.log(pessoa.__proto__);              // Object.prototype
console.log(pessoa.__proto__ === Object.prototype); // true

É crucial entender a diferença entre prototype (propriedade de funções construtoras) e [[Prototype]] (referência interna dos objetos). Funções possuem a propriedade prototype, que será atribuída como [[Prototype]] dos objetos criados com new:

function Animal(nome) {
  this.nome = nome;
}
Animal.prototype.falar = function() {
  return `${this.nome} faz algum som.`;
};

const cachorro = new Animal('Rex');
console.log(Object.getPrototypeOf(cachorro) === Animal.prototype); // true

2. Cadeia de Protótipos (Prototype Chain)

Quando acessamos uma propriedade em um objeto, o JavaScript primeiro verifica se ela existe no próprio objeto. Se não encontrar, sobe na cadeia de protótipos até encontrá-la ou chegar ao topo (null).

const avo = { nome: 'João' };
const pai = Object.create(avo);
pai.sobrenome = 'Silva';
const filho = Object.create(pai);
filho.idade = 25;

console.log(filho.idade);    // 25 (própria)
console.log(filho.sobrenome); // 'Silva' (do pai)
console.log(filho.nome);     // 'João' (do avô)
console.log(filho.toString); // função (de Object.prototype)

// Verificando a cadeia
console.log(filho instanceof Object); // true
console.log(pai.isPrototypeOf(filho)); // true

O topo da cadeia é Object.prototype, que por sua vez tem null como prototype. Isso encerra a busca.

3. Criando Objetos com Prototypes

Existem três formas principais de definir o prototype de um objeto:

Funções construtoras:

function Carro(marca) {
  this.marca = marca;
}
Carro.prototype.ligar = function() {
  return `${this.marca} ligou.`;
};

const fusca = new Carro('Volkswagen');

Object.create():

const veiculo = {
  mover() { return 'Movendo...'; }
};
const moto = Object.create(veiculo);
moto.tipo = 'esportiva';
console.log(moto.mover()); // 'Movendo...'

Literais de objeto:

const obj = {}; // prototype = Object.prototype

A diferença fundamental: new executa a função construtora e vincula o prototype; Object.create() apenas cria o objeto com o prototype especificado; literais sempre herdam de Object.prototype.

4. Herança via Prototype

Podemos implementar herança configurando manualmente a cadeia de protótipos:

function Animal(nome) {
  this.nome = nome;
}
Animal.prototype.comer = function() {
  return `${this.nome} está comendo.`;
};

function Cachorro(nome, raca) {
  Animal.call(this, nome); // herda propriedades
  this.raca = raca;
}

// Configurando herança
Cachorro.prototype = Object.create(Animal.prototype);
Cachorro.prototype.constructor = Cachorro; // corrige o construtor

// Sobrescrita (shadowing)
Cachorro.prototype.comer = function() {
  return `${this.nome} (${this.raca}) está comendo ração.`;
};

const rex = new Cachorro('Rex', 'Pastor Alemão');
console.log(rex.comer()); // 'Rex (Pastor Alemão) está comendo ração.'

O problema do constructor ocorre porque Object.create() sobrescreve o prototype, perdendo a referência correta. A linha Cachorro.prototype.constructor = Cachorro resolve isso.

5. Prototype vs Classes ES6

As classes ES6 são açúcar sintático sobre o sistema de protótipos:

// Classe ES6
class Pessoa {
  constructor(nome) {
    this.nome = nome;
  }
  saudacao() {
    return `Olá, sou ${this.nome}`;
  }
  static criar(nome) {
    return new Pessoa(nome);
  }
}

// Equivalente com prototype
function PessoaOld(nome) {
  this.nome = nome;
}
PessoaOld.prototype.saudacao = function() {
  return `Olá, sou ${this.nome}`;
};
PessoaOld.criar = function(nome) {
  return new PessoaOld(nome);
};

Herança com extends e super:

class Funcionario extends Pessoa {
  constructor(nome, cargo) {
    super(nome); // chama construtor da classe pai
    this.cargo = cargo;
  }
  saudacao() {
    return `${super.saudacao()} e trabalho como ${this.cargo}`;
  }
}

Por baixo dos panos, extends configura a cadeia de protótipos exatamente como vimos na seção anterior.

6. Boas Práticas e Performance

Adicionar métodos ao prototype economiza memória, pois todas as instâncias compartilham a mesma função:

// Ruim: cada instância tem sua própria função
function Ruim() {
  this.metodo = function() { /* ... */ };
}

// Bom: todas as instâncias compartilham
function Bom() {}
Bom.prototype.metodo = function() { /* ... */ };

Evite mutar prototypes de objetos nativos em tempo de execução (como Array.prototype.meuMetodo = ...), pois isso pode causar conflitos em bibliotecas e quebrar a previsibilidade do código.

7. Prototype no Contexto Node.js e React

Node.js: O módulo EventEmitter utiliza prototypes extensivamente:

const EventEmitter = require('events');

class MeuEmissor extends EventEmitter {
  constructor() {
    super();
  }
  disparar() {
    this.emit('evento', { dados: 'exemplo' });
  }
}
// Equivalente prototípico:
function MeuEmissorOld() {
  EventEmitter.call(this);
}
MeuEmissorOld.prototype = Object.create(EventEmitter.prototype);
MeuEmissorOld.prototype.disparar = function() {
  this.emit('evento', { dados: 'exemplo' });
};

React: Antes dos hooks, componentes de classe usavam prototypes para métodos de ciclo de vida:

class MeuComponente extends React.Component {
  constructor(props) {
    super(props);
    this.state = { contador: 0 };
  }
  incrementar() {
    this.setState({ contador: this.state.contador + 1 });
  }
  render() {
    return <button onClick={() => this.incrementar()}>{this.state.contador}</button>;
  }
}

Hoje, componentes funcionais com hooks são preferíveis, mas o entendimento de prototypes ainda é essencial para depurar código legado e compreender o funcionamento interno de bibliotecas como Redux e React Router.


Referências