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.
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
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);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 só 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:
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());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:
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));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:
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());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:
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'));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:
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()));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:
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()));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:
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()));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:
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);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:
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());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
mapque usa odescontodeclarado acima dele é uma closure — assunto da lição de função callback. useStateeuseEffectno 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
Mapno 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.
Perguntas frequentes
Closure é a função ou é o escopo que ela guardou?
Toda função em JavaScript é uma closure?
Closure causa vazamento de memória?
Qual a diferença entre closure e o campo privado com
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 — Closures — developer.mozilla.org
- ECMAScript 2026 — Environment Records — tc39.es


