Classes em JavaScript: POO com class e constructor
Constructor, campo privado com cerquilha, getter, setter e static — e por que class é açúcar sintático sobre protótipos, com os erros reais no Node 24.
class é a sintaxe usada para definir como uma família de objetos será criada e
quais comportamentos compartilhará. O constructor inicializa os dados de cada
instância; os métodos declarados no corpo ficam no prototype e podem ser
usados por todos os objetos criados com new.
Imagine uma fábrica. A classe é o projeto da peça, o constructor é a etapa que
recebe as medidas de cada encomenda e cada new Produto(...) produz uma
instância. Os métodos não são uma ferramenta nova fabricada para cada produto:
ficam numa bancada compartilhada, o Produto.prototype. De volta ao mecanismo
real, JavaScript continua usando sua cadeia de protótipos; class organiza esse
modelo e acrescenta regras como modo estrito, campos privados e proibição de
chamada sem new.
Uma classe, duas instâncias
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)}`;
}
}
const teclado = new Produto('TEC-01', 'Teclado mecânico', 289.9);
const mouse = new Produto('MOU-07', 'Mouse sem fio', 149.9);
console.log(teclado.etiqueta());
console.log(mouse.etiqueta());O constructor roda uma vez por new e é o único lugar onde os dados entram.
this aponta para o objeto recém-criado — o mesmo this de método de objeto
que aparece em funções em JavaScript, com a mesma
regra: quem chama decide quem ele é.
Nada da palavra function antes de etiqueta, e nada de vírgula separando os
membros como num objeto literal. Método não leva ponto e vírgula no fim; campo,
sim (#quantidade = 5;), porque campo é declaração e método é bloco. Dentro do
corpo da classe, a sintaxe é outra.
Onde o método realmente mora
Duas instâncias, um método. Ele não foi copiado duas vezes:
class Produto {
constructor(sku) {
this.sku = sku;
}
etiqueta() {
return this.sku;
}
}
const teclado = new Produto('TEC-01');
console.log(typeof Produto);
console.log(Object.keys(teclado));
console.log(Object.getOwnPropertyNames(Produto.prototype));
console.log(Object.getPrototypeOf(teclado) === Produto.prototype);
console.log(Object.hasOwn(teclado, 'etiqueta'), 'etiqueta' in teclado);Cinco linhas que desmontam a ilusão:
typeof Produtoé'function'. Não existe tipo “class” no JavaScript.- A instância tem só
sku. O que veio doconstructoré dela; o resto, não. etiquetamora emProduto.prototype, junto comconstructor.- A instância aponta para esse protótipo.
- Por isso a última linha diz
false true: o método não é propriedade própria da instância, mas está acessível a partir dela.
É essa indireção que faz mil instâncias ocuparem a memória de mil objetos de dados, e não de mil cópias de método. O caminho completo dessa busca é o assunto de herança e prototype.
Campo privado: a cerquilha é regra do motor
class Estoque {
#quantidade;
constructor(sku, quantidade) {
this.sku = sku;
this.#quantidade = quantidade;
}
reservar(pedidos) {
if (pedidos > this.#quantidade) return `${this.sku}: só há ${this.#quantidade} em estoque`;
this.#quantidade -= pedidos;
return `${this.sku}: reservado ${pedidos}, restam ${this.#quantidade}`;
}
}
const teclados = new Estoque('TEC-01', 5);
console.log(teclados.reservar(2));
console.log(teclados.reservar(9));
console.log(Object.keys(teclados));
console.log(JSON.stringify(teclados));O campo funcionou lá dentro e sumiu de fora: nem Object.keys nem
JSON.stringify o enxergam. Isso é diferente da velha convenção de prefixar com
underscore, que é só um pedido educado. Se o seu objeto precisa aparecer inteiro
na serialização mesmo tendo campos privados, escreva um toJSON — o mecanismo
está descrito em JSON em JavaScript.
Tentar ler o campo de fora nem chega a executar:
class Estoque {
#quantidade = 5;
}
const teclados = new Estoque();
console.log(teclados.#quantidade);Erro de sintaxe, não de execução: o motor recusa o arquivo inteiro antes de
rodar a primeira linha. A cerquilha faz parte do nome do campo — #quantidade é
o nome, e ele só existe dentro do corpo da classe que o declarou.
get e set: propriedade com regra
Um getter parece propriedade e é método. Um setter intercepta a atribuição — que é onde a validação entra:
class ItemDoCarrinho {
#quantidade = 1;
constructor(sku, preco) {
this.sku = sku;
this.preco = preco;
}
get quantidade() {
return this.#quantidade;
}
set quantidade(valor) {
if (!Number.isInteger(valor) || valor < 1) {
throw new RangeError(`quantidade inválida para ${this.sku}: ${valor}`);
}
this.#quantidade = valor;
}
get subtotal() {
return this.preco * this.#quantidade;
}
}
const item = new ItemDoCarrinho('TEC-01', 289.9);
item.quantidade = 3;
console.log(item.quantidade, item.subtotal.toFixed(2));
try {
item.quantidade = 0;
} catch (erro) {
console.log(`${erro.name}: ${erro.message}`);
}
console.log('continua valendo', item.quantidade);item.quantidade = 3 parece uma atribuição banal e passou por uma função com
validação. item.subtotal parece um campo guardado e é calculado a cada
leitura — nunca fica dessincronizado do preço nem da quantidade.
static: o que pertence à classe
class Pedido {
static FRETE_GRATIS_ACIMA_DE = 199;
static #contador = 0;
constructor(cliente, total) {
Pedido.#contador += 1;
this.numero = 8411 + Pedido.#contador;
this.cliente = cliente;
this.total = total;
}
static deCarrinho(cliente, precos) {
return new Pedido(cliente, precos.reduce((soma, preco) => soma + preco, 0));
}
static quantosForamCriados() {
return Pedido.#contador;
}
get temFreteGratis() {
return this.total >= Pedido.FRETE_GRATIS_ACIMA_DE;
}
}
const a = new Pedido('Ana Souza', 89.9);
const b = Pedido.deCarrinho('Bruno Lima', [289.9, 149.9]);
console.log(a.numero, a.total, a.temFreteGratis);
console.log(b.numero, b.total.toFixed(2), b.temFreteGratis);
console.log(Pedido.quantosForamCriados(), Pedido.FRETE_GRATIS_ACIMA_DE);
console.log(a.FRETE_GRATIS_ACIMA_DE);A última linha é a que ensina: membro static vive na classe, não nas
instâncias. a.FRETE_GRATIS_ACIMA_DE é undefined porque a instância nunca
recebeu essa propriedade — o acesso correto é Pedido.FRETE_GRATIS_ACIMA_DE.
deCarrinho é uma fábrica: um segundo jeito de construir o objeto, com nome
que explica o que ele faz. Melhor que um constructor com quatro parâmetros
opcionais e um if decidindo qual formato chegou.
Classe também tem zona morta temporal
Função declarada pode ser chamada acima da linha em que aparece. Classe, não:
const teclado = new Produto('TEC-01');
class Produto {
constructor(sku) {
this.sku = sku;
}
}
console.log(teclado);A mesma mensagem que let e const produzem, pelo mesmo motivo: o nome existe
no escopo desde o topo do bloco, mas fica inacessível até a linha da declaração.
A lição sobre variáveis detalha essa TDZ.
O contraste com função é direto:
const teclado = criarProduto('TEC-01');
function criarProduto(sku) {
return { sku };
}
console.log(teclado);Na prática isso quer dizer: declare suas classes antes de usá-las, de preferência no topo do módulo. Não conte com içamento.
Erros comuns
Esquecer o new
class Produto {
constructor(sku, nome) {
this.sku = sku;
this.nome = nome;
}
}
const teclado = Produto('TEC-01', 'Teclado mecânico');
console.log(teclado);Este é um dos ganhos concretos da sintaxe de classe. A mesma coisa escrita como
função construtora não daria erro nenhum: this viraria undefined em módulo,
o retorno seria undefined e você caçaria o bug três funções depois. A classe
falha na linha certa, com o nome certo na mensagem.
O caso mais comum não é esquecer no seu código — é passar o método como
callback e perder o new no caminho, tipo lista.map(Produto). Use
lista.map((sku) => new Produto(sku)).
Achar que o corpo da classe é código normal
Todo corpo de classe roda em modo estrito, sempre, mesmo em script antigo sem
'use strict':
class Cupom {
aplicar() {
desconto = 10;
return desconto;
}
}
console.log(new Cupom().aplicar());Fora do modo estrito, essa linha criaria uma variável global sem avisar. Dentro
da classe ela é um erro imediato — e note que o rastro traz Cupom.aplicar, o
que torna a origem óbvia. Faltou const.
Um truque que a cerquilha permite
Como campo privado só existe dentro da classe, o operador in vira um teste de
autenticidade que nenhum objeto de fora consegue falsificar:
class Pedido {
#numero;
constructor(numero) {
this.#numero = numero;
}
static ehPedido(valor) {
return #numero in valor;
}
get numero() {
return this.#numero;
}
}
const real = new Pedido(8412);
const impostor = { numero: 8412 };
console.log(Pedido.ehPedido(real));
console.log(Pedido.ehPedido(impostor));
console.log(real.numero, impostor.numero);Os dois objetos respondem 8412 a .numero, e mesmo assim a classe distingue
um do outro. Guarde esse detalhe: na próxima lição você vai ver o instanceof
falhando exatamente onde este teste acerta.
Quando a classe compensa
| situação | use |
|---|---|
| um objeto de configuração, criado uma vez | objeto literal |
| transformar dados sem guardar estado | função pura |
| muitas instâncias com o mesmo formato | classe |
| estado interno que precisa de validação | classe com campo privado e setter |
| agrupar funções soltas por assunto | módulo, não classe com tudo static |
O sinal mais confiável de que uma classe é desnecessária: ela só tem
constructor e getters, e é criada uma vez. Isso é um objeto. O sinal de que
ela é necessária: existe uma regra que precisa valer sempre — estoque não fica
negativo, quantidade é inteiro positivo — e você não quer confiar em quem for
usar o objeto.
Crie duas instâncias de uma classe Conta com um campo privado #saldo, um
método depositar que rejeita valor negativo e um getter de leitura. Confirme
três resultados: depositar em uma conta não altera a outra, acessar
conta.#saldo fora da classe nem compila e
Conta.prototype.depositar === conta.depositar é true. Assim você verifica
instância, privacidade e método compartilhado no mesmo exercício.
A trilha de JavaScript segue em herança, extends e prototype, onde a cadeia que este texto mostrou de relance vira o mecanismo central. O guia completo de JavaScript tem o mapa da trilha.
Perguntas frequentes
Se class é só açúcar, por que usar em vez de função construtora?
Campo privado com cerquilha é diferente de underscore no nome?
Quando devo usar static?
Preciso mesmo de classes em JavaScript?
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 — Classes — developer.mozilla.org
- MDN — Propriedades privadas de classe — developer.mozilla.org


