Entender escopo e closures significa entender onde as coisas dão errado. Estes são os erros que pegam até desenvolvedores experientes.
A Pergunta #1 de Entrevista sobre Closures
Esta é a armadilha clássica de closure. Quase todo mundo erra na primeira vez:
O Problema
snippetJavaScript
// O que isso imprime?for (var i = 0; i < 3; i++) { setTimeout(function() { console.log(i); }, 1000);} // A maioria espera: 0, 1, 2// Saída real: 3, 3, 3
Por Que Isso Acontece?
snippettext
O que realmente acontece: TEMPO ════════════════════════════════════════════════════► ┌─────────────────────────────────────────────────────────┐ │ IMEDIATAMENTE (milissegundos): │ │ │ │ Iteração 1 do loop: i = 0, agenda callback │ │ Iteração 2 do loop: i = 1, agenda callback │ │ Iteração 3 do loop: i = 2, agenda callback │ │ Loop termina: i = 3 │ │ │ │ Os 3 callbacks apontam para a MESMA variável 'i' ──┐ │ └──────────────────────────────────────────────────────│──┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ ~1 SEGUNDO DEPOIS: │ │ │ │ callback1 roda: "Qual é i?" → i é 3 → imprime 3 │ │ callback2 roda: "Qual é i?" → i é 3 → imprime 3 │ │ callback3 roda: "Qual é i?" → i é 3 → imprime 3 │ │ │ └─────────────────────────────────────────────────────────┘ Resultado: 3, 3, 3 (não 0, 1, 2!)
As Soluções
Solução 1: Use let
A solução moderna mais simples. let cria um novo binding para cada iteração:
snippetJavaScript
for (let i = 0; i < 3; i++) { setTimeout(function() { console.log(i); }, 1000);}// Saída: 0, 1, 2 ✓
Solução 2: IIFE
Solução pré-ES6 usando Immediately Invoked Function Expression:
snippetJavaScript
for (var i = 0; i < 3; i++) { (function(j) { setTimeout(function() { console.log(j); }, 1000); })(i); // Passa i como argumento, criando um novo 'j' cada vez}// Saída: 0, 1, 2 ✓
Solução 3: forEach
Usando métodos de array, que naturalmente criam novo escopo por iteração:
snippetJavaScript
[0, 1, 2].forEach(function(i) { setTimeout(function() { console.log(i); }, 1000);});// Saída: 0, 1, 2 ✓
Vazamentos de Memória com Closures
Closures são poderosos, mas vêm com responsabilidade. Como closures mantêm referências às variáveis do escopo externo, essas variáveis não podem ser coletadas pelo garbage collector.
Vazamentos de Memória Potenciais
snippetJavaScript
function createHeavyClosure() { const hugeData = new Array(1000000).fill('x'); // Dados grandes return function() { // Esta referência a hugeData mantém o array inteiro na memória console.log(hugeData.length); };} const leakyFunction = createHeavyClosure();// hugeData ainda está na memória porque o closure referencia
Engines modernos como o V8 podem otimizar closures que não usam variáveis externas. Porém, é boa prática assumir que variáveis referenciadas são retidas e limpar dados grandes explicitamente quando terminar.
Quebrando Referências de Closure
Quando terminar com um closure, quebre a referência explicitamente. Use removeEventListener() para limpar event handlers:
snippetJavaScript
function setupHandler(element) { const largeData = { users: new Array(10000).fill({ name: 'User' }) }; const handler = function() { console.log(`Processando ${largeData.users.length} usuários`); }; element.addEventListener('click', handler); // Retorna função de cleanup return function cleanup() { element.removeEventListener('click', handler); // Agora handler e largeData podem ser coletados };} const button = document.getElementById('myButton');const cleanup = setupHandler(button); // Depois, quando não precisar mais:cleanup(); // Remove listener, libera memória
Boas práticas:
- Não capture mais do que precisa em closures
- Defina referências de closure como
null quando terminar
- Remova event listeners quando componentes desmontarem
- Tenha cuidado especial em loops e aplicações de longa duração