search

JavaScript Herança Antes das Classes: prototype chain e Object.create()

JavaScript Herança Antes das Classes: prototype chain e Object.create()

JavaScript nasceu com um modelo de objetos baseado em protótipos. Mesmo após a chegada da sintaxe de class (ES2015), o mecanismo fundamental continua sendo o mesmo: delegação via prototype chain. Por isso, entender a herança “antes das classes” segue relevante — tanto para manter sistemas legados quanto para escrever código mais previsível e seguro.

Neste artigo, o foco é explicar JavaScript Herança Classes a partir do que acontece por baixo do capô: como a cadeia de protótipos resolve propriedades, como Object.create() cria objetos com protótipo explícito e quais armadilhas de segurança e manutenção aparecem nesse caminho.

O que é “herança” em JavaScript (sem classes)

Em linguagens clássicas orientadas a objetos, herança geralmente significa “uma classe filha herda da classe pai”. Em JavaScript, o modelo é diferente:

  • Objetos podem delegar a busca de propriedades para outro objeto (o protótipo).
  • Essa delegação é encadeada: se não achar em um objeto, procura no protótipo; se não achar, procura no protótipo do protótipo, e assim por diante.
Herança JavaScript

Esse encadeamento é chamado de prototype chain (cadeia de protótipos). Portanto, a “herança” em JavaScript é, na prática, compartilhamento e reutilização por delegação, não uma cópia de membros.

Prototype chain: como a resolução de propriedades funciona

Quando você acessa obj.algo, o motor do JavaScript segue, em termos práticos, esta ordem:

  1. Procura a propriedade algo no próprio obj (propriedade “own”).
  2. Se não existir, procura no protótipo de obj.
  3. Se ainda não existir, sobe a cadeia até chegar em null.

Um exemplo simples:

const base = {
  tipo: "base",
  describe() {
    return `Tipo: ${this.tipo}`;
  },
};

const obj = Object.create(base);
obj.tipo = "filho";

console.log(obj.describe()); // "Tipo: filho"

O método describe() não está em obj — está em base. Ao chamar obj.describe(), o JavaScript encontra describe no protótipo, mas executa com this apontando para obj. Esse detalhe é central: o método é compartilhado, mas o estado pode estar no objeto “filho”.

Verificando a cadeia na prática

Algumas formas de inspecionar isso:

console.log(Object.getPrototypeOf(obj) === base); // true
console.log(obj.hasOwnProperty("describe")); // false
console.log("describe" in obj); // true
  • hasOwnProperty verifica se a propriedade é do próprio objeto.
  • O operador in verifica se a propriedade existe em algum ponto da cadeia.

Object.create(): herança explícita por delegação

Object.create(proto) cria um novo objeto cujo protótipo é proto. Isso permite compor objetos de forma direta, sem construtores e sem new.

Padrão básico com Object.create()

const usuarioBase = {
  init(nome) {
    this.nome = nome;
    return this;
  },
  saudacao() {
    return `Olá, ${this.nome}`;
  },
};

const usuario = Object.create(usuarioBase).init("Ana");
console.log(usuario.saudacao()); // "Olá, Ana"

Esse padrão (às vezes chamado de “prototypal inheritance” ou “OLOO” — Objects Linked to Other Objects) tem algumas características:

  • usuarioBase funciona como “modelo” de comportamento.
  • Objetos concretos são criados e ligados a esse modelo.
  • Não há “classe”; há um objeto delegando para outro.

Criando com propriedades já definidas (descriptors)

Object.create também aceita um segundo argumento com property descriptors, útil para controlar writable, enumerable, configurable:

const contaBase = {
  saldo() {
    return this._saldo;
  },
};

const conta = Object.create(contaBase, {
  _saldo: { value: 100, writable: true, enumerable: false },
});

console.log(conta.saldo()); // 100
console.log(Object.keys(conta)); // []

Isso é relevante para reduzir exposição acidental de dados em loops e serializações, embora não seja um mecanismo de segurança por si só.

Antes das classes: função construtora e prototype

Historicamente, o padrão mais comum para “JavaScript Herança Classes” antes do class era a função construtora:

function Pessoa(nome) {
  this.nome = nome;
}

Pessoa.prototype.saudacao = function () {
  return `Olá, ${this.nome}`;
};

const p = new Pessoa("Bruno");
console.log(p.saudacao());
  • new cria um objeto novo.
  • Define o protótipo do novo objeto como Pessoa.prototype.
  • Executa Pessoa com this apontando para o novo objeto.

Esse modelo é prototípico, mas tem semântica mais “parecida com classe”, e por isso foi amplamente adotado.

Herança entre “tipos” com construtores (padrão clássico)

Um padrão tradicional:

function Animal(nome) {
  this.nome = nome;
}

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

function Cachorro(nome) {
  Animal.call(this, nome);
}

Cachorro.prototype = Object.create(Animal.prototype);
Cachorro.prototype.constructor = Cachorro;

Cachorro.prototype.falar = function () {
  return `${this.nome} late.`;
};

const dog = new Cachorro("Rex");
console.log(dog.falar());

O que acontece aqui, passo a passo:

  1. Animal.call(this, nome) copia a inicialização de Animal para o objeto Cachorro.
  2. Cachorro.prototype = Object.create(Animal.prototype) conecta os protótipos.
  3. Ajusta-se constructor por convenção (útil para inspeção e algumas ferramentas).
  4. Métodos específicos são adicionados em Cachorro.prototype.

Esse padrão antecede class extends, mas o mecanismo subjacente é o mesmo.

O que muda com class (e o que não muda)

A sintaxe class organiza o código e reduz armadilhas, mas:

  • class não substitui protótipos; ela apenas oferece uma camada sintática.
  • Métodos declarados em class vão para prototype.
  • extends configura a cadeia de protótipos de forma automática.

Saber isso é útil ao depurar problemas de herança, sombras de propriedades (shadowing), this e comportamento inesperado em objetos.

Armadilhas comuns (e como evitá-las)

1) Compartilhar estado mutável no protótipo

Colocar arrays/objetos no protótipo pode fazer diferentes instâncias compartilharem o mesmo estado:

const base = {
  itens: [],
};

const a = Object.create(base);
const b = Object.create(base);

a.itens.push("x");
console.log(b.itens); // ["x"] (surpresa)

Como evitar: inicialize estado mutável em cada objeto, não no protótipo:

const baseSeguro = {
  init() {
    this.itens = [];
    return this;
  },
};

const a2 = Object.create(baseSeguro).init();
const b2 = Object.create(baseSeguro).init();

2) Confundir in com “propriedade do objeto”

Para validações, in pode trazer propriedades herdadas (incluindo as injetadas na cadeia):

"toString" in {}; // true

Se o objetivo for validar apenas propriedades próprias, use Object.hasOwn() (mais moderno) ou hasOwnProperty:

const obj = Object.create(null);
obj.chave = 1;

console.log(Object.hasOwn(obj, "chave")); // true

3) Riscos de Prototype Pollution (visão de segurança)

Do ponto de vista de Cyber Segurança, a cadeia de protótipos pode ser explorada em certos cenários, especialmente quando aplicações mesclam objetos com entrada não confiável de forma ingênua. O problema mais conhecido é prototype pollution, onde chaves como __proto__, constructor e prototype podem alterar comportamentos inesperados ao se fazer merge de objetos.

Medidas práticas:

  • Evite merges “profundos” (deep merge) caseiros sem filtragem de chaves perigosas.
  • Prefira bibliotecas atualizadas e com proteção conhecida, ou implemente allowlist de chaves.
  • Para mapas/dicionários de dados vindos do usuário, considere Object.create(null) para evitar protótipo:
const dict = Object.create(null);
dict["__proto__"] = "valor"; // não altera o protótipo global
console.log(dict["__proto__"]); // "valor"

Isso não resolve todos os problemas de validação, mas reduz superfícies de ataque ligadas a herança via protótipo.

Quando usar Object.create() hoje

Cenários comuns e legítimos:

  • Criar objetos simples com delegação clara (comportamento compartilhado).
  • Implementar objetos “tipo dicionário” com Object.create(null).
  • Controlar descritores de propriedades ao criar objetos.
  • Evitar new e reduzir acoplamento a funções construtoras em designs baseados em composição.

Em código moderno, class é frequentemente preferido por legibilidade e padronização, mas Object.create() segue sendo uma ferramenta direta e expressiva para modelar delegação.

Resumo: JavaScript Herança Classes sem classes

  • A “herança” em JavaScript é delegação via prototype chain.
  • Object.create() cria objetos já conectados a um protótipo, sem new.
  • Funções construtoras com prototype foram o padrão dominante antes de class.
  • A sintaxe class é uma camada mais confortável, mas o mecanismo continua sendo prototípico.
  • Em segurança, a cadeia de protótipos exige cuidado com merges de objetos e entradas não confiáveis (prototype pollution).

Entender esses fundamentos torna mais fácil depurar comportamento de objetos, ler código legado e tomar decisões mais seguras ao lidar com dados externos e estruturas de objetos complexas.

Compartilhar este artigo: