voltar aos artigosobject-oriented js
$cd~/artigos/factories-and-classes-criar-objetos-javascript
Object-Oriented JS
série 33 Conceitos JS·parte 17 de 38

Factories & Classes: Fábricas e Classes em JavaScript

18 minVanderlei José Alamino

Como os jogos criam milhares de inimigos, como frameworks gerenciam componentes reutilizáveis, ou como sistemas bancários criam contas com segurança? Tudo gira em torno de criar objetos de forma eficiente e organizada.

No JavaScript existem duas abordagens principais: Factory Functions (funções fábrica) e Classes (ES6). Ambas resolvem o mesmo problema — objetos com estrutura consistente — mas de formas diferentes, cada uma com vantagens e armadilhas.

snippetJavaScript
// Factory — sem newconst alice = criarJogador("Alice"); // Classe — com newconst bob = new Jogador("Bob");

Neste guia da série 33 Conceitos JS — projeto open source criado por Leonardo Maldonado — você vai aprender:

  • Factory Functions, Constructor Functions e Classes ES6
  • Privacidade real com closures e campos privados #
  • Herança com extends/super e composição vs herança
  • Quando escolher Factories vs Classes na prática

Pré-requisitos: Scope and Closures e familiaridade básica com objetos JavaScript.

factories-and-classes-criar-objetos-javascript.meta.jsonjson

// metadados do artigo

{
  "tag": "object-oriented js",
  "readTime": "18 min",
  "author": "Vanderlei José Alamino",
  "source": "33jsconcepts.com",
  "series": "33 Conceitos JS",
  "part": {
    "current": 17,
    "total": 38
  }
}
abrir fonte original
factories-and-classes-criar-objetos-javascript.mdmarkdown

Fonte de referência

Conteúdo adaptado a partir do guia original em inglês.

33jsconcepts.com
01

seção

Por que Precisamos de Blueprints para Objetos?

Imagine montar um exército de peças de xadrez esculpindo cada uma à mão — lento, inconsistente e impossível de escalar. Você precisa de um molde ou de uma fábrica.

Criar objetos manualmente tem os mesmos problemas:

snippetJavaScript
// ❌ Repetitivo e propenso a errosconst jogador1 = {  nome: "Alice",  vida: 100,  nivel: 1,  atacar() { return `${this.nome} ataca!`; }}; const jogador2 = {  nome: "Bob",  vida: 100,  nivel: 1,  atacar() { return `${this.nome} ataca!`; }  // Mesmo código copiado!};

Problemas: repetição, typos, manutenção difícil (alterar em 50 lugares) e desperdício de memória (cada objeto carrega cópia dos métodos).

snippettext
┌─────────────────────────────────────────────────────────────────┐│  TRÊS FORMAS DE CRIAR OBJETOS EM JAVASCRIPT                    │├─────────────────────────────────────────────────────────────────┤│  1. CRIAÇÃO MANUAL     const obj = { ... }  — não escala        ││  2. FACTORY FUNCTION   criarJogador("Alice") — flexível, sem new││  3. CLASSE / CONSTRUCTOR  new Jogador("Alice") — instanceof     │└─────────────────────────────────────────────────────────────────┘
02

seção

Factory Functions: A Fábrica de Objetos

Uma Factory Function é uma função comum que cria e retorna um novo objeto. Não precisa de new.

Factory básica

snippetJavaScript
function criarJogador(nome) {  return {    nome,    vida: 100,    nivel: 1,    atacar() {      return `${this.nome} ataca causando ${10 + this.nivel * 2} de dano!`;    },    receberDano(dano) {      this.vida -= dano;      return this.vida <= 0        ? `${this.nome} foi derrotado!`        : `${this.nome} tem ${this.vida} de vida restante.`;    }  };} const alice = criarJogador("Alice");console.log(alice.atacar());  // "Alice ataca causando 12 de dano!"

Factory com múltiplos parâmetros

snippetJavaScript
function criarInimigo(nome, vida, poderAtaque) {  return {    nome, vida, poderAtaque, estaVivo: true,    atacar(alvo) {      return `${this.nome} ataca ${alvo.nome} causando ${this.poderAtaque} de dano!`;    },    receberDano(dano) {      this.vida -= dano;      if (this.vida <= 0) { this.vida = 0; this.estaVivo = false; }      return `${this.nome} tem ${this.vida} de vida restante.`;    }  };} const goblin = criarInimigo("Goblin", 50, 10);const dragao = criarInimigo("Dragão", 500, 50);

Factory com objeto de configuração

snippetJavaScript
function criarPersonagem(config) {  const padroes = { nome: "Desconhecido", vida: 100, vidaMaxima: 100, nivel: 1, poderAtaque: 10, defesa: 5 };  const configFinal = { ...padroes, ...config };   return {    ...configFinal,    atacar(alvo) {      const dano = Math.max(0, this.poderAtaque - alvo.defesa);      return `${this.nome} causa ${dano} de dano em ${alvo.nome}!`;    },    curar(qtd) {      this.vida = Math.min(this.vidaMaxima, this.vida + qtd);      return `${this.nome} se curou para ${this.vida} de vida.`;    }  };} const guerreiro = criarPersonagem({ nome: "Guerreiro", vida: 150, poderAtaque: 20 });const aldeao = criarPersonagem({ nome: "Aldeão" });  // usa padrões
03

seção

Privacidade Real com Closures

Um superpoder das Factory Functions: dados verdadeiramente privados via closures — como um cofre bancário acessível só pelos métodos retornados.

snippetJavaScript
function criarContaBancaria(titular, saldoInicial = 0) {  let saldo = saldoInicial;  const historicoTransacoes = [];   function registrarTransacao(tipo, valor) {    historicoTransacoes.push({ tipo, valor, saldo, data: new Date().toISOString() });  }   registrarTransacao("abertura", saldoInicial);   return {    titular,    depositar(valor) {      if (valor <= 0) throw new Error("Valor deve ser positivo");      saldo += valor;      registrarTransacao("deposito", valor);      return `Depositado $${valor}. Novo saldo: $${saldo}`;    },    sacar(valor) {      if (valor <= 0) throw new Error("Valor deve ser positivo");      if (valor > saldo) throw new Error("Saldo insuficiente");      saldo -= valor;      registrarTransacao("saque", valor);      return `Sacado $${valor}. Novo saldo: $${saldo}`;    },    consultarSaldo() { return saldo; },    extrato() {      return historicoTransacoes.map(t =>        `${t.data}: ${t.tipo} $${t.valor} (Saldo: $${t.saldo})`      ).join("\n");    }  };} const conta = criarContaBancaria("Alice", 1000);conta.depositar(500);console.log(conta.consultarSaldo());  // 1300 console.log(conta.saldo);              // undefined ❌conta.saldo = 1000000;                 // não afeta o saldo realconsole.log(conta.consultarSaldo());   // 1300 ✅

saldo e historicoTransacoes existem apenas no escopo de criarContaBancaria. Os métodos retornados acessam via closure — encapsulamento real.

04

seção

Constructor Functions: O Jeito Clássico

Antes das Classes ES6, a forma padrão era Constructor Functions com new:

snippetJavaScript
function Jogador(nome) {  this.nome = nome;  this.vida = 100;  this.nivel = 1;  this.atacar = function() {    return `${this.nome} ataca!`;  };} const alice = new Jogador("Alice");console.log(alice instanceof Jogador);  // true

O que new faz por baixo dos panos?

snippettext
PASSO 1: Cria objeto vazio          const obj = {}PASSO 2: Liga o protótipo           Object.setPrototypeOf(obj, Jogador.prototype)PASSO 3: Executa o construtor       Jogador.call(obj, "Alice")PASSO 4: Retorna o objeto           return obj

Métodos no protótipo — economia de memória

snippetJavaScript
function Jogador(nome) {  this.nome = nome;  this.vida = 100;  // ❌ Não coloque métodos aqui — cada instância recebe cópia} Jogador.prototype.atacar = function() {  return `${this.nome} ataca!`;}; const p1 = new Jogador("Alice");const p2 = new Jogador("Bob");console.log(p1.atacar === p2.atacar);  // true ✅ — compartilhado
snippettext
   Jogador.prototype   ┌─────────────────┐   │ atacar: fn      │◄── compartilhado   └────────┬────────┘            │ [[Prototype]]     ┌──────┴──────┐     ▼             ▼   p1 {nome}     p2 {nome}   ← dados próprios, métodos compartilhados

O perigo: esquecer o new

snippetJavaScript
const alice = Jogador("Alice");  // Sem new!console.log(alice);     // undefined// this vazou para global — bug difícil de rastrear// Em strict mode: TypeError

Sempre use new com construtores. Classes ES6 lançam erro automaticamente se esquecer.

05

seção

Classes ES6: Açúcar Sintático sobre Protótipos

Classes ES6 são açúcar sintático — sintaxe mais limpa, mesmo comportamento de construtores + protótipos.

snippetJavaScript
class Jogador {  constructor(nome) {    this.nome = nome;    this.vida = 100;    this.nivel = 1;  }   atacar() {    return `${this.nome} ataca causando ${10 + this.nivel * 2} de dano!`;  }   receberDano(dano) {    this.vida -= dano;    return this.vida <= 0 ? `${this.nome} foi derrotado!` : `${this.nome} tem ${this.vida} HP.`;  }} const alice = new Jogador("Alice");console.log(alice instanceof Jogador);  // true

Por baixo dos panos:

snippetJavaScript
// Classe ES6 ≡ Constructor + prototypefunction Inimigo(nome, vida) { this.nome = nome; this.vida = vida; }Inimigo.prototype.atacar = function() { return `${this.nome} ataca!`; }; console.log(typeof Inimigo);  // "function" — classes SÃO funções!

Sintaxe completa: campos, getters, setters, estáticos

snippetJavaScript
class Personagem {  nivel = 1;  experiencia = 0;   constructor(nome, vida = 100) {    this.nome = nome;    this.vida = vida;  }   atacar() { return `${this.nome} ataca!`; }   get estaVivo() { return this.vida > 0; }   set pontosDeVida(valor) {    this.vida = Math.max(0, valor);  }   static criarHeroi(nome) { return new Personagem(nome, 150); }  static NIVEL_MAXIMO = 99;} const heroi = Personagem.criarHeroi("Alice");console.log(heroi.estaVivo);           // getter — sem parêntesesconsole.log(Personagem.NIVEL_MAXIMO);  // 99

Métodos estáticos

Pertencem à classe, não às instâncias — ideais para utilitários e factory methods:

snippetJavaScript
class UtilidadesMatematicas {  static PI = 3.14159;  static quadrado(x) { return x * x; }} console.log(UtilidadesMatematicas.quadrado(5));  // 25const util = new UtilidadesMatematicas();console.log(util.quadrado);  // undefined ❌
06

seção

Campos Privados (`#`)

ECMAScript 2022 introduziu campos privados com # — verdadeiramente privados, diferente da convenção _underscore:

snippetJavaScript
class ContaBancaria {  #saldo = 0;  #pin;  #historicoTransacoes = [];   constructor(titular, saldoInicial, pin) {    this.titular = titular;    this.#saldo = saldoInicial;    this.#pin = pin;  }   #verificarPin(pin) { return this.#pin === pin; }   #registrarTransacao(tipo, valor) {    this.#historicoTransacoes.push({ tipo, valor, saldo: this.#saldo });  }   depositar(valor) {    if (valor <= 0) throw new Error("Valor inválido");    this.#saldo += valor;    this.#registrarTransacao("deposito", valor);    return this.#saldo;  }   sacar(valor, pin) {    if (!this.#verificarPin(pin)) throw new Error("PIN inválido");    if (valor > this.#saldo) throw new Error("Saldo insuficiente");    this.#saldo -= valor;    return this.#saldo;  }} // conta.#saldo → SyntaxError! ❌
AspectoCampos #Closures (Factory)
Sintaxethis.#campolet variavel na função
Erro ao acessarSyntaxErrorundefined
MemóriaMétodos via protótipo (eficiente)Cada instância tem métodos próprios
instanceofFuncionaNão funciona
HerançaPrivado por classeNão herdado
07

seção

Herança com `extends` e `super`

snippetJavaScript
class Personagem {  constructor(nome, vida) {    this.nome = nome;    this.vida = vida;  }  atacar() { return `${this.nome} ataca!`; }  receberDano(dano) {    this.vida -= dano;    return `${this.nome} tem ${this.vida} HP.`;  }} class Guerreiro extends Personagem {  constructor(nome) {    super(nome, 150);  // DEVE chamar antes de usar this    this.armadura = 20;  }  receberDano(dano) {    const reduzido = Math.max(0, dano - this.armadura);    return super.receberDano(reduzido);  }  golpeEscudo() {    return `${this.nome} golpeia com escudo causando ${this.armadura} de dano!`;  }} class Mago extends Personagem {  constructor(nome) {    super(nome, 80);    this.mana = 100;  }  atacar() {    if (this.mana >= 10) {      this.mana -= 10;      return `${this.nome} lança bola de fogo! (Mana: ${this.mana})`;    }    return `${this.nome} ataque básico de 5 de dano.`;  }} const conan = new Guerreiro("Conan");console.log(conan instanceof Guerreiro);   // trueconsole.log(conan instanceof Personagem); // true

super tem dois papéis: no construtor chama o pai (super(...)); nos métodos acessa métodos do pai (super.metodo()). Em classes derivadas, super() antes de this.

08

seção

Composição vs Herança

Herança profunda cria o Problema do Gorila-Banana: você queria uma banana, mas recebeu a selva inteira (Animal → Mamífero → Primata → Macaco → Gorila).

Composição ("tem-um") mistura comportamentos em vez de hierarquias rígidas ("é-um"):

snippetJavaScript
const podeAndar = (estado) => ({  andar() {    estado.posicao += estado.velocidade;    return `${estado.nome} anda para ${estado.posicao}`;  }}); const podeNadar = (estado) => ({  nadar() {    estado.posicao += estado.velocidade * 1.5;    return `${estado.nome} nada para ${estado.posicao}`;  }}); const podeVoar = (estado) => ({  voar() {    estado.posicao += estado.velocidade * 3;    return `${estado.nome} voa para ${estado.posicao}`;  }}); function criarPato(nome) {  const estado = { nome, posicao: 0, velocidade: 2 };  return { nome, ...podeAndar(estado), ...podeNadar(estado), ...podeVoar(estado) };} function criarPinguim(nome) {  const estado = { nome, posicao: 0, velocidade: 1 };  return { nome, ...podeAndar(estado), ...podeNadar(estado) };  // sem voar} const donald = criarPato("Donald");donald.andar(); donald.nadar(); donald.voar(); const tux = criarPinguim("Tux");// tux.voar();  // TypeError ❌
AspectoHerançaComposição
Relação"é-um""tem-um"
FlexibilidadeHierarquia rígidaMistura comportamentos
AcoplamentoForteFraco
Melhor paraHierarquias claras, instanceofCombinações flexíveis

Regra de ouro: Prefira composição sobre herança quando a relação "é-um" não for óbvia.

09

seção

Factory vs Class: Qual Usar?

CaracterísticaFactory FunctionClasse ES6
SintaxeFunção comumclass
newNão necessárioObrigatório
instanceofNão funcionaFunciona
PrivacidadeClosuresCampos #
MemóriaMétodos por instânciaMétodos via protótipo
HerançaComposiçãoextends

Use Factory quando: precisa de composição flexível, privacidade via closures, estilo funcional, ou não precisa de instanceof.

Use Classe quando: precisa de instanceof, hierarquia "é-um" clara, familiaridade OOP da equipe, ou requisitos de framework.

snippettext
Precisa de instanceof?  SIM → ClasseHierarquia clara?       SIM → Classe com extendsMisturar comportamentos? SIM → Factory + composiçãoPrivacidade real?       SIM → Factory (closures) OU Classe (#)
10

seção

Erros Comuns

Esquecer new com construtores

snippetJavaScript
const alice = Jogador("Alice");  // this vaza para globalconst bob = new Jogador("Bob");  // ✅// Classes lançam TypeError se esquecer new

Esquecer super() em classes derivadas

snippetJavaScript
class Cachorro extends Animal {  constructor(nome, raca) {    // this.raca = raca;  // ReferenceError!    super(nome);          // ✅ primeiro    this.raca = raca;  }}

Achar que _underscore é privado

snippetJavaScript
conta._saldo = 999999;  // totalmente acessível 😱// Use #saldo para privacidade real

this perdido em Factory Functions

snippetJavaScript
const incrementar = contador.incrementar;incrementar();  // this quebrado! // ✅ Use closure em vez de this:function criarContadorSeguro() {  let contagem = 0;  return { incrementar() { contagem++; }, getContagem() { return contagem; } };}

Em classes, arrow functions como campos ligam this automaticamente:

snippetJavaScript
class Botao {  contagem = 0;  handleClick = () => { this.contagem++; };}
11

seção

Perguntas Clássicas de Entrevista

12

seção

Teste Seu Conhecimento

13

seção

Próximos Passos

Você dominou as duas formas principais de criar objetos em JavaScript. O próximo conceito na trilha Object-Oriented JS é this, call, apply e bind — entender como o contexto funciona em diferentes situações, especialmente dentro de classes e callbacks.

Depois disso, mergulhe em Object Creation & Prototypes para ver a cadeia de protótipos por baixo das classes, e Inheritance & Polymorphism para padrões avançados de OOP.

→ this, call, apply & bind — contexto em JavaScript

Fonte de referência: 33jsconcepts.com/concepts/factories-and-classes ⬀

$ summary --quick

Resumo rápido

  • Factory Functions retornam objetos — simples, flexíveis, sem new
  • Constructor Functions + new criam instâncias — abordagem tradicional
  • Classes ES6 são açúcar sintático sobre construtores e protótipos
  • new cria objeto, liga protótipo, executa construtor e retorna
  • Métodos de protótipo são compartilhados — economia de memória
  • Campos # e closures fornecem privacidade real
  • Métodos estáticos pertencem à classe, não às instâncias
  • Herança (extends) para hierarquias "é-um"; composição para flexibilidade
  • Use classes para instanceof e OOP; factories para composição e estilo funcional

Créditos da série

Este artigo faz parte de uma série de estudos inspirada no projeto 33 Conceitos JS, criado por Leonardo Maldonado. Todo o conteúdo original está disponível em 33jsconcepts.com.