O bug clássico do loop e a memoização
Um erro tão comum que virou piada entre programadores: um loop que deveria imprimir 0, 1, 2, mas imprime 3, 3, 3. Entender por que isso acontece é o teste definitivo de que você pegou a ideia de closure.
Estudar esta lição interativa →Imagine que você quer criar três funções, uma para cada posição de um loop, e cada uma deveria "lembrar" o seu próprio número: a primeira mostra 0, a segunda mostra 1, a terceira mostra 2. Parece trivial, mas quem já caiu nessa pegadinha sabe: dependendo de como o loop foi escrito, as três funções mostram 3, 3, 3, o mesmo número errado, repetido. Esse é, sem exagero, um dos bugs mais famosos do JavaScript, e ele é puro efeito de closure.
// O bug clássico: var é compartilhado por todas as voltas do loop.
function comVar() {
const fns = [];
for (var i = 0; i < 3; i++) {
fns.push(() => console.log("var:", i));
}
fns.forEach((fn) => fn());
}
comVar(); // var: 3, var: 3, var: 3 (todas viram o valor final de i)
// Com let, cada volta ganha a sua PRÓPRIA variável.
function comLet() {
const fns = [];
for (let i = 0; i < 3; i++) {
fns.push(() => console.log("let:", i));
}
fns.forEach((fn) => fn());
}
comLet(); // let: 0, let: 1, let: 2A explicação está em quantas variáveis existem. O var é o jeito antigo de declarar variáveis (o let e o const vieram depois, em parte para evitar problemas como este) e não respeita o bloco { } do loop: existe uma única variável i, compartilhada por todas as voltas. As três funções guardadas em fns fecham sobre essa mesma variável, e quando finalmente são chamadas, o loop já terminou e i vale 3 para todas elas. O let, ao contrário, cria uma variável nova a cada volta: cada função fecha sobre a sua variável, a da volta em que nasceu, e ninguém mais mexe nela, então o valor fica 0, 1 ou 2. A regra da closure é a mesma de sempre (ela guarda a variável, não uma foto do valor); o que muda é que, com let, são três variáveis diferentes.
É como três amigos anotando um recado num quadro-negro compartilhado (o var) contra cada um escrevendo no seu próprio post-it (o let). No quadro-negro, só existe um número por vez, e quem olhar por último vê sempre o valor mais recente. Nos post-its, cada um tem o seu número, e como ninguém mais escreve no post-it dos outros, ele continua igual.
Desde o Módulo 2 você já usa let e const em vez de var, então, na prática, esse bug específico não vai te pegar. Mas ele é o exemplo mais didático que existe para provar que closures guardam referências, não fotos, exatamente o aviso da lição passada. Se um dia você ler código antigo com var num loop e ver comportamento estranho perto de funções, esse é o primeiro suspeito.
Fechando a lição, outro uso clássico e muito prático de closures: a memoização (guardar em cache resultados já calculados). Pense numa função que faz uma conta pesada, algo que demora de verdade. Se ela for chamada várias vezes com o mesmo argumento, calcular tudo de novo é desperdício. Uma closure pode guardar um "cofre" com os resultados já vistos e, na próxima vez que o mesmo argumento aparecer, devolver a resposta na hora, sem recalcular:
// Memoização: uma closure guarda um cache dos resultados já calculados.
function criarCalculadoraCara() {
const cache = {};
return function calcular(n) {
if (n in cache) {
console.log("veio do cache:", n);
return cache[n];
}
console.log("calculando de verdade:", n);
let resultado = 0;
for (let i = 0; i <= n; i++) resultado += i; // "conta pesada" fictícia
cache[n] = resultado;
return resultado;
};
}
const calcular = criarCalculadoraCara();
console.log(calcular(1000)); // calculando de verdade
console.log(calcular(1000)); // veio do cache, sem recalcular
console.log(calcular(500)); // calculando de verdade (argumento novo)Cache não é mágica de graça: o objeto cache da closure cresce a cada argumento novo, e nunca encolhe sozinho. Numa função chamada com infinitos argumentos diferentes, isso vira um vazamento de memória disfarçado. Memoização compensa quando os mesmos argumentos se repetem bastante (uma busca com poucos termos populares, por exemplo), não quando cada chamada é única.
// Sua vez! Troque SÓ o var por let no cabeçalho do loop e rode de novo.
function comVar() {
const fns = [];
for (var i = 0; i < 3; i++) {
fns.push(() => console.log("resultado:", i));
}
fns.forEach((fn) => fn());
}
comVar();Troque var por let e clique em Rodar.