Fechamento JavaScript dentro de loops - exemplo simples e prático

var funcs = [];
// let's create 3 functions
for (var i = 0; i < 3; i++) {
  // and store them in funcs
  funcs[i] = function() {
    // each should log its value.
    console.log("My value: " + i);
  };
}
for (var j = 0; j < 3; j++) {
  // and now let's run each one to see
  funcs[j]();
}

Isto resulta nisto:

O meu valor: 3 O meu valor: 3 O meu valor: 3

Considerando que I'gostaria que ele saísse:

Meu valor: 0 O meu valor: 1 O meu valor: 2


O mesmo problema ocorre quando o atraso na execução da função é causado pelo uso de ouvintes de eventos:

var buttons = document.getElementsByTagName("button");
// let's create 3 functions
for (var i = 0; i < buttons.length; i++) {
  // as event listeners
  buttons[i].addEventListener("click", function() {
    // each should log its value.
    console.log("My value: " + i);
  });
}
<button>0</button>
<br />
<button>1</button>
<br />
<button>2</button>

... ou código assíncrono, por exemplo, usando Promessas:

// Some async wait function
const wait = (ms) => new Promise((resolve, reject) => setTimeout(resolve, ms));

for (var i = 0; i < 3; i++) {
  // Log `i` as soon as each promise resolves.
  wait(i * 100).then(() => console.log(i));
}

Qual é a solução para este problema básico?

Solução

Bem, o problema é que a variável i, dentro de cada uma de suas funções anônimas, está vinculada à mesma variável fora da função.

Solução clássica: Fechamentos

O que você quer fazer é ligar a variável dentro de cada função a um valor separado e imutável fora da função:

var funcs = [];

function createfunc(i) {
  return function() {
    console.log("My value: " + i);
  };
}

for (var i = 0; i < 3; i++) {
  funcs[i] = createfunc(i);
}

for (var j = 0; j < 3; j++) {
  // and now let's run each one to see
  funcs[j]();
}

Como não há escopo de bloco no JavaScript - apenas escopo de função - ao envolver a criação da função em uma nova função, você garante que o valor de "i" permaneça como você pretendia.


2015 Solução: forEach

Com a disponibilidade relativamente ampla da função Array.prototype.forEach' (em 2015), it's vale notar que naquelas situações que envolvem iteração principalmente sobre um conjunto de valores,.forEach()` fornece uma forma limpa e natural de obter um fechamento distinto para cada iteração. Ou seja, assumindo que você'tem algum tipo de array contendo valores (referências DOM, objetos, o que quer que seja), e o problema de configurar callbacks específicos para cada elemento, você pode fazer isso:

var someArray = [ /* whatever */ ];
// ...
someArray.forEach(function(arrayElement) {
  // ... code code code for this one element
  someAsynchronousFunction(arrayElement, function() {
    arrayElement.doSomething();
  });
});

A idéia é que cada invocação da função de callback utilizada com o laço `.forEach' será o seu próprio fechamento. O parâmetro passado para aquele manipulador é o elemento de array específico para aquele passo particular da iteração. Se it's for utilizado em um callback assíncrono, ele ganhou't colide com qualquer um dos outros callbacks estabelecidos em outros passos da iteração.

Se você estiver trabalhando em jQuery, a função $.cada() lhe dá uma capacidade semelhante.


ES6 solução: let

ECMAScript 6 (ES6) introduz novas palavras-chave let' econst' que são escopadas de forma diferente das variáveis baseadas em var'. Por exemplo, em um loop com um índice baseado emlet, cada iteração através do loop terá um novo valor dei` onde cada valor é escopado dentro do loop, então seu código funcionaria como você espera. Existem muitos recursos, mas I'd recomenda 2ality's block-scoping post como uma grande fonte de informação.

for (let i = 0; i < 3; i++) {
  funcs[i] = function() {
    console.log("My value: " + i);
  };
}

Cuidado, porém, que o IE9-IE11 e o Edge antes do Edge 14 suportam let', mas errem o acima (eles não'não criam um novoi' a cada vez, então todas as funções acima seriam logadas 3 como se utilizássemos `var'). O Edge 14 finalmente acerta.

Comentários (23)

Tente:

var funcs = [];

for (var i = 0; i < 3; i++) {
    funcs[i] = (function(index) {
        return function() {
            console.log("My value: " + index);
        };
    }(i));
}

for (var j = 0; j < 3; j++) {
    funcs[j]();
}

Editar (2014):

Pessoalmente eu acho que @Aust's resposta mais recente sobre o uso do .bind é a melhor maneira de fazer esse tipo de coisa agora. There's also lo-dash/underscore's _.partial when you don't need or want to mess with bind's thisArg.

Comentários (3)

O motivo pelo qual seu exemplo original não funcionou é que todos os fechamentos que você criou no loop referenciavam o mesmo quadro. Na verdade, tendo 3 métodos em um objeto com apenas uma única variável `i'. Todos eles imprimiram o mesmo valor.

Comentários (0)