- O que é uma closure, sem rodeios
- Por que funções "lembram" de variáveis
- Closures no código do dia a dia
- O erro clássico com closures em loops
- Casos de uso reais
- Closures e performance: o que monitorar
- FAQ
- Próximos passos
O que é uma closure, sem rodeios
Toda vez que você cria uma função dentro de outra função, a função interna carrega consigo o escopo da externa. Esse "pacote" — função + variáveis do escopo onde ela foi criada — é o que chamamos de closure.
Simples assim.
function criarContador() {
let count = 0;
return function () {
count++;
return count;
};
}
const contador = criarContador();
console.log(contador()); // 1
console.log(contador()); // 2
console.log(contador()); // 3
A variável count não existe mais no escopo global. Mas a função retornada ainda tem acesso a ela — porque foi criada dentro do escopo onde count vivia. Isso é uma closure.
Por que funções "lembram" de variáveis
Quando o JavaScript executa criarContador(), cria um novo contexto de execução com count = 0. Quando criarContador termina, esse contexto normalmente seria descartado pelo garbage collector.
Mas não nesse caso. Por quê?
Porque a função interna — aquela que foi retornada — ainda mantém uma referência ao ambiente onde foi criada. O garbage collector é inteligente o suficiente para perceber isso e não liberar a memória enquanto a referência existir.
function criarSaudacao(prefixo) {
// `prefixo` fica "vivo" enquanto a função retornada existir
return function (nome) {
return `${prefixo}, ${nome}!`;
};
}
const ola = criarSaudacao("Olá");
const hey = criarSaudacao("Hey");
console.log(ola("Rafael")); // "Olá, Rafael!"
console.log(hey("Ana")); // "Hey, Ana!"
Cada chamada a criarSaudacao cria uma closure diferente, com seu próprio prefixo. As duas coexistem em memória sem interferência.
💡 Dica: Closures não compartilham estado entre si. Cada instância da função externa gera um ambiente independente.
Closures no código do dia a dia
Você já usa closures o tempo todo, mesmo sem perceber. Callbacks, handlers de evento, funções de configuração — tudo isso costuma depender de closures.
// Handler de botão com closure
function configurarBotao(elemento, mensagem) {
elemento.addEventListener("click", function () {
// `mensagem` vem do escopo externo — closure
alert(mensagem);
});
}
const btn = document.querySelector("#meu-btn");
configurarBotao(btn, "Formulário enviado com sucesso!");
No React, hooks como useState e useCallback também são closure-driven. O próprio useEffect com dependências é um exemplo direto: quando você captura um valor do escopo do componente dentro do efeito, está formando uma closure.
// React — closure com estado
function Contador() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
// `count` capturado via closure
console.log(`Valor atual: ${count}`);
setCount(count + 1);
}, [count]);
return <button onClick={handleClick}>{count}</button>;
}
Essa é, aliás, a raiz do famoso bug de "stale closure" no React — quando a closure captura um valor antigo e o componente não re-renderiza como esperado. Mas isso é papo para outro post.
O erro clássico com closures em loops
Esse aqui apareceu num PR meu anos atrás. O revisor não disse nada — o bug só apareceu em teste manual, porque todos os botões faziam a mesma coisa.
// ❌ Todos os handlers imprimem 3
for (var i = 0; i < 3; i++) {
document.querySelectorAll(".btn")[i].addEventListener("click", function () {
console.log(i); // sempre 3
});
}
O problema: var tem escopo de função, não de bloco. Quando o loop termina, i vale 3. Todas as closures criadas dentro do loop apontam para a mesma variável i.
// ✓ Cada handler tem seu próprio `i`
for (let i = 0; i < 3; i++) {
document.querySelectorAll(".btn")[i].addEventListener("click", function () {
console.log(i); // 0, 1 ou 2 — correto
});
}
let tem escopo de bloco. A cada iteração, o JavaScript cria um novo binding de i, e cada closure captura o seu próprio.
Antes do ES6, o padrão era criar uma IIFE (Immediately Invoked Function Expression) para forçar esse isolamento:
// Solução pré-ES6 com IIFE
for (var i = 0; i < 3; i++) {
(function (indice) {
document.querySelectorAll(".btn")[indice].addEventListener("click", function () {
console.log(indice);
});
})(i);
}
Funciona pelo mesmo princípio: cada chamada da IIFE cria um escopo novo, com seu próprio indice.
⚠️ Atenção: Se você ainda usa
varem código novo, esse bug vai aparecer em algum momento. Prefira sempreleteconst.
Casos de uso reais
Módulos e encapsulamento
Closures são a base do padrão de módulo — criar estado privado sem depender de classes:
function criarCarrinho() {
const itens = []; // privado
return {
adicionar(produto) {
itens.push(produto);
},
total() {
return itens.reduce((acc, item) => acc + item.preco, 0);
},
listar() {
return [...itens]; // retorna cópia, não referência
},
};
}
const carrinho = criarCarrinho();
carrinho.adicionar({ nome: "Teclado", preco: 350 });
carrinho.adicionar({ nome: "Mouse", preco: 150 });
console.log(carrinho.total()); // 500
Ninguém de fora acessa itens diretamente. A closure garante isso.
Memoização simples
function memoizar(fn) {
const cache = {}; // fecha sobre esse cache
return function (...args) {
const chave = JSON.stringify(args);
if (cache[chave] !== undefined) {
return cache[chave];
}
cache[chave] = fn(...args);
return cache[chave];
};
}
const fib = memoizar(function (n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
});
console.log(fib(40)); // rápido — resultados em cache
Partial application e currying
function multiplicar(a) {
return function (b) {
return a * b;
};
}
const dobrar = multiplicar(2);
const triplicar = multiplicar(3);
console.log(dobrar(5)); // 10
console.log(triplicar(5)); // 15
Útil para criar variações de funções sem repetir lógica.
Closures e performance: o que monitorar
Closures mantêm referências ao escopo externo vivas. Se esse escopo contém objetos grandes, a memória não é liberada enquanto a closure existir.
// ❌ Problema em potencial
function processarDados(dados) {
const resultadoGrande = dados.map(/* operação pesada */);
return function relatorio() {
// `resultadoGrande` nunca será coletado enquanto `relatorio` existir
return resultadoGrande.length;
};
}
// ✓ Captura só o que precisa
function processarDados(dados) {
const resultadoGrande = dados.map(/* operação pesada */);
const tamanho = resultadoGrande.length; // captura só o número
return function relatorio() {
return tamanho; // `resultadoGrande` pode ser coletado
};
}
Na prática, esse problema aparece em aplicações que criam muitas closures com referências a dados grandes — workers, processamento batch, listeners que nunca são removidos. Nos casos do dia a dia, não tem de que se preocupar.
FAQ
Toda função em JavaScript é uma closure? Tecnicamente sim — toda função mantém uma referência ao escopo onde foi definida. Na prática, falamos de closure quando essa referência ao escopo externo é o que torna a função útil ou interessante. Uma função que não usa nenhuma variável do escopo externo forma uma closure vazia, sem efeito observável.
Closure é a mesma coisa que escopo léxico? Não exatamente. Escopo léxico é a regra que define quais variáveis uma função pode acessar com base em onde foi escrita — não onde é executada. Closure é o mecanismo que mantém esse escopo acessível mesmo depois que a função externa terminou. Um depende do outro, mas não são sinônimos.
Por que meu useEffect no React está usando um valor desatualizado?
Stale closure. O efeito capturou um valor do estado em um render anterior e não foi re-executado quando o estado mudou. A solução quase sempre é adicionar a dependência correta no array de deps do useEffect, ou usar useRef para valores que precisam ser lidos sempre com o valor atual sem re-executar o efeito.
Closure tem custo de performance relevante? Em condições normais, não. O custo real aparece quando closures capturam referências a objetos grandes e impedem o garbage collector de agir — especialmente em loops que criam milhares de closures ou em listeners que nunca são removidos. Para o código de aplicação típico, esse custo é desprezível.
Quando devo preferir closure ao invés de uma classe?
Quando você precisa de encapsulamento simples e não precisa de herança, polimorfismo ou instanciação múltipla com new. Módulos baseados em closure são mais simples de raciocinar, sem o this que muda de contexto dependendo de como a função é chamada.
Próximos passos
Closures são a base de vários padrões avançados em JavaScript. Agora que o conceito está claro, os próximos temas naturais são:
- Currying e partial application — transformar funções de múltiplos argumentos em cadeias de funções unárias
- Stale closures no React — por que o
useEffectcaptura valores antigos e como resolver comuseRefe dependências corretas - Module pattern — encapsulamento real sem TypeScript
privateou#do ES2022 - Memoização avançada — quando e como aplicar cache em funções puras sem biblioteca
Se quiser ver mais posts sobre fundamentos JavaScript aplicados na prática, o blog da Dev Code cobre event loop, promises, async/await e escopo com a mesma abordagem direta.