Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

Herança em JavaScript: extends, super e prototype

A herança do JavaScript é prototipal, não baseada em classes. Veja a cadeia impressa, por que super vem antes de this e onde o instanceof mente.

Rodolfo Mori7 min de leitura

Em JavaScript, um objeto não herda de uma classe: ele aponta para outro objeto. Esse objeto apontado é o protótipo, e quando você lê uma propriedade que o objeto não tem, o motor sobe por essa ligação até achar — ou até acabar a cadeia e devolver undefined. Herança aqui é uma busca, não uma cópia.

Resposta curta: extends liga o prototype de uma classe ao da outra, e super chama o construtor de cima. É a mesma mecânica de Object.create, escrita de um jeito que se parece com Java. Quem entende a cadeia depura qualquer código; quem só decorou extends trava no primeiro comportamento estranho.

Esta lição continua o checkout das anteriores, agora com produto físico e produto digital dividindo o mesmo catálogo.

Essa ligação recebe o nome de prototype chain, ou cadeia de protótipos. Em palavras simples, quando a propriedade não está no objeto atual, o JavaScript continua a busca no objeto apontado por ele.

A busca sobe a lista de responsáveis

Imagine um atendente procurando quem pode autorizar um reembolso. Se a pessoa do balcão não tem essa permissão, a consulta sobe para a supervisão e depois para a gerência, até encontrar uma resposta ou acabar a lista. A leitura de uma propriedade percorre a cadeia do mesmo modo. extends e super organizam essas ligações, mas não trocam a mecânica por outra.

Antes do primeiro exemplo, escolha uma propriedade própria e uma herdada. Para cada uma, preveja hasOwn, o valor lido e o objeto da cadeia onde a busca deve parar. Depois confira com Object.getPrototypeOf. Assim você enxerga a herança como busca concreta, não como cópia invisível.

js
class Produto {
  constructor(sku, nome, preco) {
    this.sku = sku;
    this.nome = nome;
    this.preco = preco;
  }

  etiqueta() {
    return `${this.sku} — ${this.nome}: R$ ${this.preco.toFixed(2)}`;
  }

  frete() {
    return 12.5;
  }
}

class ProdutoDigital extends Produto {
  constructor(sku, nome, preco, tamanhoMb) {
    super(sku, nome, preco);
    this.tamanhoMb = tamanhoMb;
  }

  frete() {
    return 0;
  }

  etiqueta() {
    return `${super.etiqueta()} · download de ${this.tamanhoMb} MB`;
  }
}

const teclado = new Produto('TEC-01', 'Teclado mecânico', 289.9);
const ebook = new ProdutoDigital('EBK-03', 'Guia de JavaScript', 39.9, 12);

console.log(teclado.etiqueta(), '| frete', teclado.frete());
console.log(ebook.etiqueta(), '| frete', ebook.frete());
TEC-01 — Teclado mecânico: R$ 289.90 | frete 12.5 EBK-03 — Guia de JavaScript: R$ 39.90 · download de 12 MB | frete 0

super aparece em dois papéis diferentes no mesmo arquivo. No construtor, é uma chamada: super(...) executa o construtor da classe de cima. Num método, é um objeto: super.etiqueta() chama a versão do pai e ainda passa o this da instância — foi assim que o método da base conseguiu ler this.sku.

super antes de this não é estilo, é regra

js
class Produto {
  constructor(sku) {
    this.sku = sku;
  }
}

class ProdutoDigital extends Produto {
  constructor(sku, tamanhoMb) {
    this.tamanhoMb = tamanhoMb;
    super(sku);
  }
}

console.log(new ProdutoDigital('EBK-03', 12));
file:///private/tmp/checkout/this-antes-do-super.mjs:9 this.tamanhoMb = tamanhoMb; ^ ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor at new ProdutoDigital (file:///private/tmp/checkout/this-antes-do-super.mjs:9:5) at file:///private/tmp/checkout/this-antes-do-super.mjs:14:13 Node.js v24.16.0

A mensagem é literal, e o motivo é estrutural: numa classe derivada, quem constrói o objeto é a classe base. Até o super rodar, this não está vazio — ele não existe, exatamente como uma variável let antes da declaração. Por isso o erro é ReferenceError, e não TypeError.

Invertendo as duas linhas, tudo funciona:

js
class Produto {
  constructor(sku) {
    this.sku = sku;
  }
}

class ProdutoDigital extends Produto {
  constructor(sku, tamanhoMb) {
    super(sku);
    this.tamanhoMb = tamanhoMb;
  }
}

console.log(new ProdutoDigital('EBK-03', 12));
ProdutoDigital { sku: 'EBK-03', tamanhoMb: 12 }

A cadeia, impressa

Dá para percorrer a ligação na mão e ver o caminho que o motor faz a cada leitura de propriedade:

js
class Produto {
  etiqueta() {
    return this.sku;
  }
}
class ProdutoDigital extends Produto {}

const ebook = new ProdutoDigital();
ebook.sku = 'EBK-03';

let atual = ebook;
const cadeia = [];
while (atual !== null) {
  cadeia.push(atual.constructor?.name ?? '(sem constructor)');
  atual = Object.getPrototypeOf(atual);
}
console.log(cadeia.join(' -> '));

console.log(Object.getPrototypeOf(ebook) === ProdutoDigital.prototype);
console.log(Object.getPrototypeOf(ProdutoDigital.prototype) === Produto.prototype);
console.log(Object.getPrototypeOf(ProdutoDigital) === Produto);
console.log(Object.hasOwn(ebook, 'etiqueta'), Object.hasOwn(Produto.prototype, 'etiqueta'));
ProdutoDigital -> ProdutoDigital -> Produto -> Object true true true false true

A primeira linha aparece com ProdutoDigital duas vezes, e isso não é bug: o primeiro é a instância, o segundo é ProdutoDigital.prototype — que herda a propriedade constructor e por isso responde com o mesmo nome. Depois vem Produto.prototype, depois Object.prototype, depois null. Fim da linha.

ligação o que é
ebookProdutoDigital.prototype onde os métodos da subclasse moram
ProdutoDigital.prototypeProduto.prototype é isso que extends monta
ProdutoDigitalProduto herança dos membros static
Produto.prototypeObject.prototype toString, hasOwnProperty e afins

Repare na terceira linha da tabela: extends cria duas ligações, uma entre os protótipos e outra entre as próprias funções. É por isso que método static também é herdado.

etiqueta não está na instância — está em Produto.prototype, dois saltos acima. Cada leitura de ebook.etiqueta percorre esse caminho. Se o método estivesse na instância, a lição de classes em JavaScript já teria mostrado ele em Object.keys.

Herança sem classe nenhuma

Para provar que a classe é sintaxe, aqui está a mesma hierarquia montada só com objetos e Object.create:

js
const produto = {
  etiqueta() {
    return `${this.sku} — ${this.nome}`;
  },
  frete() {
    return 12.5;
  },
};

const produtoDigital = Object.create(produto);
produtoDigital.frete = function () {
  return 0;
};

const ebook = Object.create(produtoDigital);
ebook.sku = 'EBK-03';
ebook.nome = 'Guia de JavaScript';

console.log(ebook.etiqueta(), '| frete', ebook.frete());
console.log(produto.isPrototypeOf(ebook), produtoDigital.isPrototypeOf(ebook));
console.log(Object.keys(ebook));
EBK-03 — Guia de JavaScript | frete 0 true true [ 'sku', 'nome' ]

Nenhuma classe, nenhum new, e o comportamento é idêntico: o frete da camada do meio venceu, o etiqueta veio de dois níveis acima, e a instância só tem os dados dela. Isto é a herança do JavaScript. class é a forma de escrever isso sem errar o encanamento.

O jeito pré-2015, que você ainda encontra em código legado e em bibliotecas antigas, é o mesmo mecanismo com a mão mais suja:

js
function Produto(sku, preco) {
  this.sku = sku;
  this.preco = preco;
}

Produto.prototype.etiqueta = function () {
  return `${this.sku}: R$ ${this.preco.toFixed(2)}`;
};

function ProdutoDigital(sku, preco, tamanhoMb) {
  Produto.call(this, sku, preco);
  this.tamanhoMb = tamanhoMb;
}

ProdutoDigital.prototype = Object.create(Produto.prototype);
ProdutoDigital.prototype.constructor = ProdutoDigital;

const ebook = new ProdutoDigital('EBK-03', 39.9, 12);

console.log(ebook.etiqueta(), ebook.tamanhoMb);
console.log(ebook instanceof ProdutoDigital, ebook instanceof Produto);
EBK-03: R$ 39.90 12 true true

Produto.call(this, ...) é o super(...). As duas linhas que reatribuem prototype são o extends. Esquecer a linha do constructor é o clássico dessa era: a herança funciona, mas ebook.constructor.name passa a dizer Produto.

Quem ganha a busca: sombreamento

A busca para no primeiro objeto da cadeia que tem a propriedade. Dá para ver isso ao vivo, mudando a cadeia com o programa rodando:

js
class Produto {
  frete() {
    return 12.5;
  }
}
class ProdutoDigital extends Produto {}

const ebook = new ProdutoDigital();
console.log('herdado:', ebook.frete());

ProdutoDigital.prototype.frete = function () {
  return 0;
};
console.log('depois de definir na subclasse:', ebook.frete());

ebook.frete = () => 999;
console.log('depois de definir na instância:', ebook.frete());

delete ebook.frete;
console.log('depois do delete:', ebook.frete());
herdado: 12.5 depois de definir na subclasse: 0 depois de definir na instância: 999 depois do delete: 0

A instância ganha do protótipo da subclasse, que ganha do protótipo da base. E como o objeto já existente enxergou a mudança feita depois de ele ser criado, fica claro que ele não copiou nada: ele consulta a cadeia a cada chamada.

instanceof pode mentir

instanceof não pergunta “de que tipo é isto”. Ele caminha a cadeia procurando um prototype específico — e uma cadeia é só um conjunto de ponteiros.

O caso mais comum acontece quando o valor nasceu em outro contexto de execução:

js
import { runInNewContext } from 'node:vm';

const carrinhoDeOutroContexto = runInNewContext('[289.9, 149.9]');

console.log(Array.isArray(carrinhoDeOutroContexto));
console.log(carrinhoDeOutroContexto instanceof Array);
console.log(carrinhoDeOutroContexto.length, carrinhoDeOutroContexto[0]);
true false 2 289.9

É um array de verdade: tem length, tem índice, Array.isArray confirma. Mas instanceof Array diz false, porque o Array daquele contexto é outra função, com outro prototype. Na web, isso acontece com dados vindos de um iframe; no Node, com vm e com alguns workers.

O contrário também vale — dá para forjar um true:

js
class Pedido {
  constructor(numero) {
    this.numero = numero;
  }
  total() {
    return 289.9;
  }
}

const impostor = { numero: 8412 };
Object.setPrototypeOf(impostor, Pedido.prototype);

console.log(impostor instanceof Pedido);
console.log(impostor.total());
console.log(impostor.constructor.name);

class Aberta {
  static [Symbol.hasInstance]() {
    return true;
  }
}
console.log('uma string é Aberta?', 'Teclado mecânico' instanceof Aberta);
true 289.9 Pedido uma string é Aberta? true

O impostor nunca passou pelo construtor e mesmo assim responde true, executa os métodos e se apresenta como Pedido. E o último caso mostra que Symbol.hasInstance permite reescrever o significado do operador — uma string declarada instância de uma classe.

quero saber não use use
é array? x instanceof Array Array.isArray(x)
é uma instância real da minha classe? x instanceof Minha campo privado com #campo in x
tem este método? instanceof typeof x.metodo === 'function'
qual o tipo primitivo? instanceof typeof x

O teste com campo privado é aquele que a lição de classes mostrou: ele não pode ser forjado porque o campo não existe fora do corpo da classe que o declarou.

super e static fora do lugar esperado

super funciona em qualquer método definido com sintaxe curta, mesmo em objeto literal:

js
const produto = {
  descricao() {
    return `${this.nome} por R$ ${this.preco.toFixed(2)}`;
  },
};

const ebook = {
  __proto__: produto,
  nome: 'Guia de JavaScript',
  preco: 39.9,
  descricao() {
    return `${super.descricao()} (entrega imediata)`;
  },
};

console.log(ebook.descricao());
console.log(Object.getPrototypeOf(ebook) === produto);
Guia de JavaScript por R$ 39.90 (entrega imediata) true

Aqui aparece o terceiro nome da família. prototype é propriedade da função construtora — o objeto que as futuras instâncias vão herdar. __proto__ é o elo que um objeto já criado tem para o protótipo dele, e num literal ele é a única forma de declarar esse elo na hora da criação. Fora desse caso, leia o elo com Object.getPrototypeOf e escreva com Object.create: __proto__ é herança histórica da linguagem, mantida por compatibilidade, e mexer nele depois que o objeto existe custa caro em desempenho.

E o static, por causa da segunda ligação que extends cria, é herdado junto — inclusive com this apontando para a subclasse dentro de uma fábrica:

js
class Produto {
  static FRETE_PADRAO = 12.5;
  static de(sku) {
    return new this(sku);
  }
  constructor(sku) {
    this.sku = sku;
  }
}

class ProdutoDigital extends Produto {
  static FRETE_PADRAO = 0;
}

const fisico = Produto.de('TEC-01');
const digital = ProdutoDigital.de('EBK-03');

console.log(fisico.constructor.name, Produto.FRETE_PADRAO);
console.log(digital.constructor.name, ProdutoDigital.FRETE_PADRAO);
console.log(digital instanceof ProdutoDigital, digital instanceof Produto);
Produto 12.5 ProdutoDigital 0 true true

ProdutoDigital não declarou de, e mesmo assim devolveu um ProdutoDigital. O new this(sku) dentro do método static usa a classe pela qual o método foi chamado — um this que não é instância, é a própria classe.

Antes de escrever extends

Herança é a ferramenta certa quando existe uma relação é um estável e a subclasse só especializa comportamento. Ela envelhece mal quando vira compartilhamento de código: aí você acaba com uma base cheia de métodos que metade das subclasses não usa.

Três perguntas que evitam a hierarquia errada:

  • Toda subclasse pode substituir a base sem surpresa? Se ProdutoDigital precisar lançar erro em algum método herdado, a relação não é “é um”.
  • Estou herdando para reaproveitar código ou para especializar tipo? Para reaproveitar, prefira compor: um objeto de estratégia passado no construtor, ou mesclagem com spread.
  • Quantos níveis? Dois é o limite prático. No terceiro, ninguém mais responde de onde veio um método sem abrir três arquivos.

Com isto você fecha o bloco de orientação a objetos da trilha de JavaScript. O que vem depois — código assíncrono, módulos, ferramentas — está mapeado no guia completo de JavaScript, e o material por tecnologia fica reunido na página de JavaScript.

  • heranca
  • prototype
  • extends
  • super
  • instanceof

Perguntas frequentes

Qual a diferença entre prototype e __proto__?
prototype é uma propriedade da função construtora: o objeto que as futuras instâncias vão herdar. __proto__ é o elo que cada objeto já criado tem para o seu protótipo. Em código novo, leia esse elo com Object.getPrototypeOf, não com __proto__.
Por que super precisa vir antes de this?
Porque numa classe derivada quem cria o objeto é o construtor da classe base. Antes do super, this ainda não existe — não é que esteja vazio, ele está numa zona morta, e tocar nele lança ReferenceError.
instanceof é confiável para validar dados?
Não. Ele apenas caminha a cadeia de protótipos, e essa cadeia pode ser trocada com Object.setPrototypeOf ou vir de outro contexto de execução. Para arrays use Array.isArray; para objetos seus, um campo privado com cerquilha e o operador in.
Quantos níveis de herança são demais?
Na prática, dois. A partir do terceiro nível fica difícil responder de onde veio um método sem abrir três arquivos, e mudanças na base quebram subclasses distantes. Composição com spread ou com objetos de estratégia envelhece melhor.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. MDN — Herança e cadeia de protótipos — developer.mozilla.org
  2. MDN — extends — developer.mozilla.org

Continue por aqui