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

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.

Rodolfo Mori6 min de leitura

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

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)}`;
  }
}

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());
TEC-01 — Teclado mecânico: R$ 289.90 MOU-07 — Mouse sem fio: R$ 149.90

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:

js
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);
function [ 'sku' ] [ 'constructor', 'etiqueta' ] true false true

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 do constructor é dela; o resto, não.
  • etiqueta mora em Produto.prototype, junto com constructor.
  • 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

js
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));
TEC-01: reservado 2, restam 3 TEC-01: só há 3 em estoque [ 'sku' ] {"sku":"TEC-01"}

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:

js
class Estoque {
  #quantidade = 5;
}

const teclados = new Estoque();
console.log(teclados.#quantidade);
file:///private/tmp/checkout/campo-privado-de-fora.mjs:6 console.log(teclados.#quantidade); ^ SyntaxError: Private field '#quantidade' must be declared in an enclosing class Node.js v24.16.0

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:

js
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);
3 869.70 RangeError: quantidade inválida para TEC-01: 0 continua valendo 3

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

js
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);
8412 89.9 false 8413 439.80 true 2 199 undefined

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:

js
const teclado = new Produto('TEC-01');

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

console.log(teclado);
file:///private/tmp/checkout/classe-tdz.mjs:1 const teclado = new Produto('TEC-01'); ^ ReferenceError: Cannot access 'Produto' before initialization at file:///private/tmp/checkout/classe-tdz.mjs:1:17 Node.js v24.16.0

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:

js
const teclado = criarProduto('TEC-01');

function criarProduto(sku) {
  return { sku };
}

console.log(teclado);
{ sku: 'TEC-01' }

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

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

const teclado = Produto('TEC-01', 'Teclado mecânico');
console.log(teclado);
file:///private/tmp/checkout/classe-sem-new.mjs:8 const teclado = Produto('TEC-01', 'Teclado mecânico'); ^ TypeError: Class constructor Produto cannot be invoked without 'new' at file:///private/tmp/checkout/classe-sem-new.mjs:8:17 Node.js v24.16.0

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':

js
class Cupom {
  aplicar() {
    desconto = 10;
    return desconto;
  }
}

console.log(new Cupom().aplicar());
file:///private/tmp/checkout/classe-modo-estrito.mjs:3 desconto = 10; ^ ReferenceError: desconto is not defined at Cupom.aplicar (file:///private/tmp/checkout/classe-modo-estrito.mjs:3:14) at file:///private/tmp/checkout/classe-modo-estrito.mjs:8:25 Node.js v24.16.0

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:

js
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);
true false 8412 8412

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.

  • classes
  • poo
  • constructor
  • campo privado
  • static

Perguntas frequentes

Se class é só açúcar, por que usar em vez de função construtora?
Porque o açúcar traz garantias que a função construtora não tinha: modo estrito por padrão, recusa de chamada sem new, campos realmente privados e uma sintaxe que todo mundo lê igual. O mecanismo por baixo é o mesmo; o que muda é quantos erros passam.
Campo privado com cerquilha é diferente de underscore no nome?
Completamente. O underscore é combinado entre humanos e qualquer código lê. A cerquilha é imposta pelo motor: acessar de fora nem compila, o campo não aparece em Object.keys e não é serializado pelo JSON.stringify.
Quando devo usar static?
Quando o comportamento pertence ao conceito e não a uma instância: constantes de configuração, contadores e fábricas como Pedido.deCarrinho. Se o método não usa this de instância, ele provavelmente é static.
Preciso mesmo de classes em JavaScript?
Não para tudo. Objeto literal e função resolvem a maioria dos casos. A classe compensa quando existem várias instâncias com o mesmo formato, um estado interno que precisa ser protegido e regras de validação que devem andar junto com os dados.

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 — Classes — developer.mozilla.org
  2. MDN — Propriedades privadas de classe — developer.mozilla.org

Continue por aqui