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.
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.
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());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
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));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:
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));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:
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'));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 é |
|---|---|
ebook → ProdutoDigital.prototype |
onde os métodos da subclasse moram |
ProdutoDigital.prototype → Produto.prototype |
é isso que extends monta |
ProdutoDigital → Produto |
herança dos membros static |
Produto.prototype → Object.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:
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));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:
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);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:
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());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:
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]);É 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:
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);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:
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);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:
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);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
ProdutoDigitalprecisar 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.
Perguntas frequentes
Qual a diferença entre prototype e __proto__?
Por que super precisa vir antes de this?
instanceof é confiável para validar dados?
Quantos níveis de herança são demais?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
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
- MDN — Herança e cadeia de protótipos — developer.mozilla.org
- MDN — extends — developer.mozilla.org


