Closures na prática: fábricas e módulos
1. Revisão essencial: o que é um closure?
Um closure é uma função interna que "lembra" do escopo léxico onde foi criada, mesmo após esse escopo ter sido executado. Em termos práticos, é a capacidade de uma função acessar variáveis de seu escopo pai mesmo depois que a função pai já terminou de executar.
function criarCumprimento() {
const mensagem = "Olá";
return function(nome) {
return `${mensagem}, ${nome}!`; // mensagem vem do closure
};
}
const saudacao = criarCumprimento();
console.log(saudacao("Maria")); // "Olá, Maria!"
Aqui, a função interna retida na variável saudacao mantém acesso à variável mensagem mesmo após criarCumprimento ter sido executada. Esse é o closure em ação.
Closures estão intimamente relacionados ao escopo de bloco. Enquanto var ignora blocos (if, for), let e const respeitam escopo de bloco, criando closures mais previsíveis.
2. Fábricas de funções com closures
O padrão factory function utiliza closures para gerar funções com comportamento personalizado sem a necessidade de classes.
function criarSaudacao(saudacao) {
return function(nome) {
return `${saudacao}, ${nome}!`;
};
}
const saudarEmPortugues = criarSaudacao("Olá");
const saudarEmIngles = criarSaudacao("Hello");
const saudarEmEspanhol = criarSaudacao("Hola");
console.log(saudarEmPortugues("João")); // "Olá, João!"
console.log(saudarEmIngles("John")); // "Hello, John!"
Cada função retornada mantém seu próprio closure com o valor de saudacao. Isso permite reutilizar lógica sem classes, seguindo um paradigma mais funcional.
3. Encapsulamento de estado privado
Closures permitem criar variáveis verdadeiramente privadas, inacessíveis do lado de fora.
function criarContador() {
let valor = 0; // variável privada
return {
incrementar: function() {
valor++;
},
decrementar: function() {
valor--;
},
obterValor: function() {
return valor;
}
};
}
const contador = criarContador();
contador.incrementar();
contador.incrementar();
console.log(contador.obterValor()); // 2
console.log(contador.valor); // undefined - não acessível
Compare com a versão usando classe ES6:
class Contador {
#valor = 0; // campo privado (ES2022)
incrementar() { this.#valor++; }
obterValor() { return this.#valor; }
}
Ambos resolvem o mesmo problema. Use closures quando precisar de simplicidade e evitar this. Use classes quando precisar de herança ou integração com frameworks que esperam classes.
4. Módulos com o padrão IIFE + closures
O padrão IIFE (Immediately Invoked Function Expression) combinado com closures foi a base dos módulos antes do ES6.
const gerenciadorDeTarefas = (function() {
// Estado privado
const tarefas = [];
// API pública
return {
adicionar: function(titulo) {
tarefas.push({ titulo, concluida: false });
},
listar: function() {
return tarefas.map(t => t.titulo);
},
concluir: function(titulo) {
const tarefa = tarefas.find(t => t.titulo === titulo);
if (tarefa) tarefa.concluida = true;
},
obterConcluidas: function() {
return tarefas.filter(t => t.concluida).map(t => t.titulo);
}
};
})();
gerenciadorDeTarefas.adicionar("Estudar closures");
gerenciadorDeTarefas.adicionar("Praticar React");
gerenciadorDeTarefas.concluir("Estudar closures");
console.log(gerenciadorDeTarefas.listar());
O array tarefas permanece inacessível externamente, expondo apenas a API definida no retorno.
5. Padrão Módulo em Node.js (CommonJS)
No Node.js, cada arquivo é um módulo CommonJS que utiliza closures implicitamente. O escopo do módulo isola variáveis do escopo global.
// contador.js
let valor = 0; // privado ao módulo
function incrementar() {
valor++;
}
function obterValor() {
return valor;
}
module.exports = { incrementar, obterValor };
// app.js
const contador = require('./contador');
contador.incrementar();
console.log(contador.obterValor()); // 1
console.log(contador.valor); // undefined
O Node.js também faz cache de módulos via require(). Se o mesmo módulo for carregado múltiplas vezes, o closure mantém o estado compartilhado — útil para singletons, mas perigoso se você espera instâncias independentes.
6. Closures no ecossistema React
React Hooks dependem fortemente de closures. useState e useEffect capturam valores no momento da renderização.
function ContadorComAtraso() {
const [count, setCount] = useState(0);
// PROBLEMA: closure obsoleto
useEffect(() => {
const timer = setTimeout(() => {
console.log(count); // sempre 0, mesmo após cliques
}, 3000);
return () => clearTimeout(timer);
}, []); // dependências vazias
return <button onClick={() => setCount(c => c + 1)}>Clique</button>;
}
Esse é o famoso closure trap: a função dentro do setTimeout capturou count como 0 e nunca atualiza.
Soluções:
// 1. Adicionar dependência correta
useEffect(() => {
const timer = setTimeout(() => console.log(count), 3000);
return () => clearTimeout(timer);
}, [count]);
// 2. Usar useRef para valor mutável
const countRef = useRef(count);
countRef.current = count;
useEffect(() => {
const timer = setTimeout(() => console.log(countRef.current), 3000);
return () => clearTimeout(timer);
}, []);
// 3. UseCallback com dependências
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
7. Armadilhas comuns e boas práticas
Memory leaks: Closures que retêm referências a objetos grandes podem impedir o garbage collector de liberar memória.
function processarDados() {
const dadosEnormes = new Array(1000000).fill("dado");
return function() {
// Se esta função nunca for chamada, dadosEnormes ainda está na memória
console.log(dadosEnormes.length);
};
}
Closures em loops com var:
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100); // 5, 5, 5, 5, 5
}
// Correção com let (escopo de bloco)
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100); // 0, 1, 2, 3, 4
}
Debugging: No DevTools do Chrome/Node.js, inspecione closures na aba "Sources" ou "Memory". Use console.dir(funcao) para ver o [[Scopes]] da função.
8. Casos de uso no mundo real
Middleware em Express:
function autenticar(permissaoRequerida) {
return function(req, res, next) {
// Closure captura permissaoRequerida
if (req.usuario.permissao >= permissaoRequerida) {
next();
} else {
res.status(403).send("Acesso negado");
}
};
}
app.get("/admin", autenticar(3), adminHandler);
Componentes de ordem superior (HOC) em React:
function comLogging(WrappedComponent) {
return function(props) {
// Closure captura WrappedComponent
console.log("Renderizando:", WrappedComponent.name);
return <WrappedComponent {...props} />;
};
}
Debounce com fábrica de funções:
function debounce(fn, atraso) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), atraso);
};
}
const buscarDados = debounce(() => {
console.log("Buscando dados...");
}, 500);
Referências
- MDN Web Docs: Closures — Documentação oficial da Mozilla sobre closures em JavaScript, com exemplos práticos e explicações detalhadas.
- Node.js Documentation: Modules — Documentação oficial do Node.js sobre o sistema de módulos CommonJS e como closures são utilizados no cache de módulos.
- React Docs: Hooks at a Glance — Guia oficial do React sobre Hooks, incluindo como closures afetam
useState,useEffecteuseCallback. - JavaScript.info: Closure — Tutorial completo e interativo sobre closures, com exemplos de fábricas de funções e módulos IIFE.
- Dan Abramov: A Complete Guide to useEffect — Artigo aprofundado sobre closures no React, explicando o "closure trap" e como resolver com dependências corretas.
- Patterns.dev: Module Pattern — Explicação visual e prática do padrão módulo com closures, incluindo exemplos em JavaScript puro e Node.js.