Por que seu código JavaScript se comporta como um telefone sem fio?
Imagine: você escreve um loop simples, adiciona manipuladores de eventos aos botões e, de repente — bum! — todos os botões mostram o mesmo número. Familiar? Se você já enfrentou fechamentos em loop JavaScript, conhece esse momento de dissonância cognitiva. Você tem certeza de que o código está correto, mas o navegador insiste: "Não, amigo, você errou alguma coisa".
No Stack Overflow, essa pergunta acumulou centenas de milhares de visualizações. E não é surpresa: capturar variáveis do loop em um fechamento é uma das armadilhas mais comuns para iniciantes e até mesmo para desenvolvedores experientes. Vamos entender por que isso acontece e, principalmente, como corrigir isso considerando o desempenho.
O problema clássico: var e assincronicidade
Veja este código inofensivo:
for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); // O que será exibido? }, 100);}Se você respondeu "0, 1, 2, 3, 4" — parabéns, você é um otimista. A realidade é cruel: o console mostrará 5, 5, 5, 5, 5. Por quê? Porque var tem escopo de função, não escopo de bloco. No momento em que o setTimeout é executado, o loop já terminou e i é igual a 5. Todos os cinco fechamentos referenciam a mesma variável.
Este é um exemplo clássico do problema de fechamentos em loop JavaScript. E a solução está na superfície: é necessário criar um novo escopo para cada iteração.
Solução nº 1: IIFE — a velha e boa muleta
Antes do ES6, os desenvolvedores usavam funções imediatamente invocadas (IIFE):
for (var i = 0; i < 5; i++) { (function(j) { setTimeout(function() { console.log(j); }, 100); })(i);}Aqui, j é uma cópia de i no momento da chamada da IIFE. Cada fechamento captura seu próprio valor. Funciona? Sim. Bonito? Nem tanto. Do ponto de vista da otimização de fechamentos, isso cria uma função extra em cada iteração, o que pode impactar a memória se o loop for enorme.
Solução nº 2: let — o salvador do ES6
Com o surgimento do let, o problema foi resolvido de forma elegante:
for (let i = 0; i < 5; i++) { setTimeout(function() { console.log(i); // 0, 1, 2, 3, 4 }, 100);}let cria uma nova vinculação para cada iteração do loop. Este é um comportamento nativo, definido na especificação ECMAScript. Cada fechamento recebe sua própria cópia de i. Desempenho? Excelente — o motor otimiza isso em nível de compilação.
No entanto, há uma ressalva: se você usar let dentro do loop, mas fora do bloco for, o comportamento pode ser diferente. Sempre mantenha o let dentro do cabeçalho do loop.
Solução nº 3: bind — abordagem funcional
O método bind permite fixar argumentos:
for (var i = 0; i < 5; i++) { setTimeout(function(j) { console.log(j); }.bind(null, i), 100);}Aqui, bind cria uma nova função com um argumento predefinido. Isso funciona, mas cria uma função extra em cada iteração. Em termos de desempenho, bind é um pouco mais lento que a IIFE devido às verificações internas.
Solução nº 4: forEach e outros métodos de array
Às vezes, a melhor maneira de evitar o problema é não usar for:
[0, 1, 2, 3, 4].forEach(function(i) { setTimeout(function() { console.log(i); }, 100);});forEach cria um novo escopo para cada iteração através do callback. Isso não apenas resolve