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

Closure em JavaScript explicado com exemplos

A função lembra o escopo onde nasceu. Contador privado, fábrica de funções e o laço com var que imprime 3,3,3 — tudo executado no Node com a saída real.

Rodolfo Mori7 min de leitura

Closure é a combinação de uma função com o ambiente léxico em que ela foi criada. Por causa dessa ligação, a função continua acessando variáveis externas mesmo depois que a execução que as criou terminou. Em linguagem direta: a função mantém acesso ao lugar onde nasceu.

Pense num crachá que continua abrindo uma sala depois que a reunião acabou. A função carrega o crachá; o ambiente léxico é a sala; as variáveis são os itens guardados lá. O crachá não leva uma fotografia dos valores: ele preserva acesso ao ambiente vivo, por isso uma chamada pode atualizar um contador e a próxima enxerga o valor novo. Tecnicamente, é uma referência ao registro de ambiente, mantida enquanto a função ainda puder ser alcançada.

Isso não é um recurso ativado por comando. Toda função nasce ligada ao seu escopo; o nome “closure” se torna especialmente útil quando ela é devolvida, guardada ou agendada e sobrevive à execução externa.

Esta lição só faz sentido depois de escopo em JavaScript, porque closure é a cadeia de escopos vista de outro ângulo: em vez de “quem eu enxergo daqui”, a pergunta vira “o que eu levo comigo quando saio”.

A função lembra o escopo onde nasceu

js
function criarSaudacao(loja) {
  return function (cliente) {
    return `${cliente}, boas-vindas à ${loja}`;
  };
}

const saudarClubStore = criarSaudacao('Club Store');
const saudarFilial = criarSaudacao('Club Store SP');

console.log(saudarClubStore('Ana Souza'));
console.log(saudarFilial('Bruno Lima'));
console.log('loja existe aqui fora?', typeof loja);
Ana Souza, boas-vindas à Club Store Bruno Lima, boas-vindas à Club Store SP loja existe aqui fora? undefined

criarSaudacao já tinha terminado quando as duas funções internas foram chamadas. Mesmo assim cada uma soube o valor de loja — e soube um valor diferente. Não existe uma variável loja compartilhada: cada chamada de criarSaudacao criou um ambiente novo, e a função devolvida ficou grudada no seu.

Do lado de fora, loja não existe em lugar nenhum. Esse é o ponto: o valor está acessível pela função que o capturou.

Contador privado: estado que ninguém alcança

O uso mais direto de closure é guardar estado sem expor a variável. Aqui o array itens fica trancado dentro de criarCarrinho:

js
function criarCarrinho() {
  let itens = [];

  return {
    adicionar(item) {
      itens.push(item);
      return itens.length;
    },
    total() {
      return Number(itens.reduce((soma, item) => soma + item.preco, 0).toFixed(2));
    },
    limpar() {
      itens = [];
    },
  };
}

const carrinhoDaAna = criarCarrinho();
const carrinhoDoBruno = criarCarrinho();

carrinhoDaAna.adicionar({ nome: 'Teclado mecânico', preco: 289.9 });
carrinhoDaAna.adicionar({ nome: 'Mouse sem fio', preco: 149.9 });
carrinhoDoBruno.adicionar({ nome: 'Mousepad', preco: 39.9 });

console.log(carrinhoDaAna.total(), carrinhoDoBruno.total());
carrinhoDaAna.limpar();
console.log(carrinhoDaAna.total(), carrinhoDoBruno.total());
439.8 39.9 0 39.9

Os três métodos devolvidos compartilham o mesmo itens — é o mesmo ambiente para os três. Mas o carrinho da Ana e o do Bruno têm ambientes separados: limpar um não tocou no outro.

Repare no limpar: ele reatribui itens, e por isso a variável precisou ser let. Se fosse const, seria preciso usar itens.length = 0. Closure captura a variável, não uma cópia do valor — mudanças feitas por qualquer uma das três funções são vistas pelas outras duas.

Fábrica de funções

Quando você chama a mesma função com configurações diferentes e guarda os resultados, tem uma fábrica:

js
function criarCalculadoraDeFrete(regiao) {
  const tabela = { sudeste: 12.5, sul: 18.9, nordeste: 27.4 };
  const base = tabela[regiao] ?? 39.9;

  return (peso) => Number((base + peso * 1.5).toFixed(2));
}

const freteSudeste = criarCalculadoraDeFrete('sudeste');
const freteNordeste = criarCalculadoraDeFrete('nordeste');
const freteDesconhecido = criarCalculadoraDeFrete('antártida');

console.log(freteSudeste(2), freteNordeste(2), freteDesconhecido(2));
15.5 30.4 42.9

A busca na tabela aconteceu uma vez por região, na criação. As três funções resultantes só fazem uma conta — não repetem a consulta a cada chamada. É a vantagem prática de separar “configurar” de “executar”.

Esse desenho é o mesmo de bibliotecas que você já usou: express() devolvendo um app configurado, um cliente de API criado com a chave e devolvendo métodos prontos, um logger criado com o nome do módulo.

Cada chamada cria um ambiente novo

Vale insistir nisso, porque é a fonte de metade da confusão com closure:

js
function criarNumerador(prefixo) {
  let sequencia = 0;
  return () => `${prefixo}-${String(++sequencia).padStart(4, '0')}`;
}

const numeroDePedido = criarNumerador('PED');
const numeroDeNota = criarNumerador('NF');

console.log(numeroDePedido(), numeroDePedido(), numeroDePedido());
console.log(numeroDeNota(), numeroDePedido());
PED-0001 PED-0002 PED-0003 NF-0001 PED-0004

numeroDeNota começou do 1 enquanto numeroDePedido já estava no 3, e a chamada seguinte de pedido continuou de onde tinha parado. Duas closures da mesma função, dois contadores independentes.

O mesmo mecanismo serve para cache. Aqui o Map e o contador de consultas vivem dentro do closure e não aparecem em lugar nenhum da API pública:

js
function comCache(calcular) {
  const cache = new Map();
  let chamadas = 0;

  return (cep) => {
    if (cache.has(cep)) return `${cache.get(cep)} (cache)`;
    chamadas += 1;
    const valor = calcular(cep);
    cache.set(cep, valor);
    return `${valor} (consulta ${chamadas})`;
  };
}

const consultarFrete = comCache((cep) => (cep.startsWith('0') ? 12.5 : 27.4));

console.log(consultarFrete('01310100'));
console.log(consultarFrete('50030230'));
console.log(consultarFrete('01310100'));
12.5 (consulta 1) 27.4 (consulta 2) 12.5 (cache)

O bug clássico: var num laço

Este é o exercício que aparece em entrevista há dez anos, e quase sempre é apresentado com setTimeout — o que dá a impressão errada de que o problema é assincronia. Não é. Veja sem nenhum agendamento:

js
const precos = [19.9, 49.9, 129.9];
const etiquetas = [];

for (var i = 0; i < precos.length; i++) {
  etiquetas.push(() => `índice ${i} → ${precos[i]}`);
}

console.log(etiquetas.map((montar) => montar()));
[ 'índice 3 → undefined', 'índice 3 → undefined', 'índice 3 → undefined' ]

Três funções, todas dizendo índice 3. O motivo: var i cria uma variável para a função inteira. As três closures capturaram a mesma variável, não três cópias do valor. Quando elas finalmente rodaram, o laço já tinha terminado e i valia 3 — daí o precos[3], que não existe.

Guarde a frase: closure captura a variável, não o valor. Todo o resto desta seção sai daí.

Por que let resolve

Trocar uma palavra muda o resultado inteiro:

js
const precos = [19.9, 49.9, 129.9];
const etiquetas = [];

for (let i = 0; i < precos.length; i++) {
  etiquetas.push(() => `índice ${i} → ${precos[i]}`);
}

console.log(etiquetas.map((montar) => montar()));
[ 'índice 0 → 19.9', 'índice 1 → 49.9', 'índice 2 → 129.9' ]

Com let, a especificação manda criar uma ligação nova a cada volta do laço e copiar para ela o valor do fim da volta anterior. São três variáveis i, uma por iteração — e portanto três ambientes distintos para as três closures.

Antes de let existir, a saída era criar o escopo na mão, com uma IIFE. Vale conhecer, porque esse padrão está em todo código de 2013:

js
const precos = [19.9, 49.9, 129.9];
const etiquetas = [];

for (var i = 0; i < precos.length; i++) {
  etiquetas.push(
    (function (indice) {
      return () => `índice ${indice} → ${precos[indice]}`;
    })(i),
  );
}

console.log(etiquetas.map((montar) => montar()));
[ 'índice 0 → 19.9', 'índice 1 → 49.9', 'índice 2 → 129.9' ]

A IIFE recebe i como parâmetro, e parâmetro é uma variável nova por chamada. Cada volta ganhou seu próprio indice. É exatamente o que o let faz hoje, de graça.

Erros comuns: tentar ler o que ficou privado

O erro característico do closure não é uma exceção dentro dele — é a exceção que acontece fora, quando alguém tenta alcançar o que o closure trancou:

js
function criarCarrinho() {
  const itens = [];
  return {
    adicionar(item) {
      itens.push(item);
      return itens.length;
    },
  };
}

const carrinho = criarCarrinho();
carrinho.adicionar({ nome: 'Teclado mecânico', preco: 289.9 });

console.log(carrinho.itens.length);
file:///private/tmp/loja/closure-privado-erro.mjs:14 console.log(carrinho.itens.length); ^

TypeError: Cannot read properties of undefined (reading ‘length’) at file:///private/tmp/loja/closure-privado-erro.mjs:14:28 at ModuleJob.run (node:internal/modules/esm/module_job:439:25)

Node.js v24.16.0

carrinho.itens é undefined porque o objeto devolvido só tem adicionar. O array existe, está vivo e é usado a cada chamada — só não tem nome acessível de fora. Essa é a privacidade que o closure entrega, e o erro é o preço de esquecer que ela existe.

A correção é decidir o que a API expõe. Se itens precisa ser lido, devolva um método que entregue uma cópia — nunca o array original, ou a privacidade some:

js
function criarCarrinho() {
  const itens = [];
  return {
    adicionar(item) {
      itens.push(item);
      return itens.length;
    },
    listar() {
      return [...itens];
    },
  };
}

const carrinho = criarCarrinho();
carrinho.adicionar({ nome: 'Teclado mecânico', preco: 289.9 });
carrinho.listar().push({ nome: 'invasor', preco: 0 });

console.log(carrinho.listar());
[ { nome: 'Teclado mecânico', preco: 289.9 } ]

O push no resultado de listar() alterou a cópia e foi embora. Se listar devolvesse itens direto, o invasor teria entrado.

Esse Cannot read properties of undefined é a mesma família de erro que aparece em array vazio, resposta de API e ordem assíncrona.

Onde closure aparece no seu dia a dia

Você usa closure muito antes de estudar o nome:

  • Todo callback que lê uma variável de fora. Um map que usa o desconto declarado acima dele é uma closure — assunto da lição de função callback.
  • useState e useEffect no React. O array de dependências existe justamente porque a função capturou valores da renderização em que nasceu.
  • Middleware de servidor. (req, res, next) => {} criado dentro de uma função de configuração enxerga as opções passadas ali.
  • Debounce e throttle. O identificador do timer mora numa variável capturada, fora da função devolvida.
  • Módulo com estado. Um arquivo que exporta funções e guarda um Map no topo é a mesma ideia, com o escopo do módulo no lugar do escopo da função.

Duas cautelas para fechar. Uma closure pode manter alcançáveis os valores que capturou — um listener que usa um objeto grande e nunca é removido pode segurar esse objeto na memória. Os motores fazem otimizações internas, então não conte com a retenção de todo o escopo como regra. E closure não substitui classe: quando o estado passa de três ou quatro campos e você precisa de herança ou de muitas instâncias, class com campo #privado lê melhor.

Faça duas chamadas separadas a uma fábrica criarContador(), guarde os retornos em contadorA e contadorB e incremente A duas vezes e B uma. Os resultados devem ser 2 e 1. Se um contador interferir no outro, a variável foi colocada fora da fábrica; se ambos preservarem seus valores, você confirmou que cada closure mantém seu próprio ambiente.

A última lição desta parte da trilha de JavaScript trata do único nome que não segue o escopo léxico: this em JavaScript. O guia completo de JavaScript tem a ordem de estudo inteira.

  • closure
  • escopo
  • estado privado
  • factory
  • laco

Perguntas frequentes

Closure é a função ou é o escopo que ela guardou?
Formalmente, é o par: a função mais o ambiente léxico em que ela foi criada. No dia a dia as pessoas chamam a função de closure, e tudo bem — desde que fique claro que o que importa é o escopo grudado nela, e não a função em si.
Toda função em JavaScript é uma closure?
Tecnicamente sim, toda função carrega uma referência ao escopo onde nasceu. O termo só ganha utilidade quando a função sobrevive ao escopo que a criou — quando ela é devolvida, guardada ou agendada e continua lendo variáveis que já deveriam ter sumido.
Closure causa vazamento de memória?
Pode causar, se você guardar num escopo de vida longa uma função que captura algo pesado. Enquanto a função for alcançável, o escopo capturado não é coletado. O caso comum é listener registrado e nunca removido, não a closure em si.
Qual a diferença entre closure e o campo privado com
O # é privacidade de linguagem, verificada pelo motor e visível na sintaxe. Closure é privacidade por escopo: não existe nome para acessar o valor de fora. Em classe, prefira #; closure continua sendo a escolha natural em função fábrica e em módulo.

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 — Closures — developer.mozilla.org
  2. ECMAScript 2026 — Environment Records — tc39.es

Continue por aqui