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

2819

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]();
}

Isso gera o seguinte:

Meu valor: 3
Meu valor: 3
Meu valor: 3

Considerando que eu gostaria que a saída:

Meu valor: 0
Meu valor: 1
Meu valor: 2


O mesmo problema ocorre quando o atraso na execução da função é causado pelo uso de listeners 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 Promises:

// 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));
}

Edit: Também é aparente no for ine for ofloops:

const arr = [1,2,3];
const fns = [];

for(i in arr){
  fns.push(() => console.log(`index: ${i}`));
}

for(v of arr){
  fns.push(() => console.log(`value: ${v}`));
}

for(f of fns){
  f();
}

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

nickf
fonte
55
Tem certeza de que não deseja funcsser uma matriz, se estiver usando índices numéricos? Apenas um aviso.
DanMan 26/07
23
Este é realmente um problema confuso. Este artigo me ajuda a entendê-lo . Pode ajudar os outros também.
User3199690
4
Outra solução simples e explicada: 1) As funções aninhadas têm acesso ao escopo "acima" delas ; 2) uma solução de fechamento ... "Um fechamento é uma função que tem acesso ao escopo pai, mesmo após o fechamento da função pai".
Peter Krauss
2
Consulte este link para obter melhores Unserstanding javascript.info/tutorial/advanced-functions
Saurabh Ahuja
35
No ES6 , uma solução trivial é declarar a variável i com let , cujo escopo é o corpo do loop.
Tomas Nikodym

Respostas:

2149

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

Solução clássica: Fechamentos

O que você deseja fazer é vincular a variável dentro de cada função a um valor inalterado separado 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 - somente escopo de função - envolvendo a criação da função em uma nova função, você garante que o valor de "i" permaneça como pretendido.


Solução ES5.1: forEach

Com a disponibilidade relativamente ampla da Array.prototype.forEachfunção (em 2015), vale a pena notar que nas situações que envolvem iteração principalmente sobre uma matriz de valores, .forEach()fornece uma maneira limpa e natural de obter um fechamento distinto para cada iteração. Ou seja, supondo que você tenha algum tipo de matriz contendo valores (referências DOM, objetos, qualquer que seja), e o problema surge ao configurar retornos de chamada 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 chamada da função de retorno de chamada usada com o .forEachloop seja seu próprio fechamento. O parâmetro passado para esse manipulador é o elemento da matriz específico para essa etapa específica da iteração. Se for usado em um retorno de chamada assíncrono, ele não colidirá com nenhum dos outros retornos de chamada estabelecidos em outras etapas da iteração.

Se você estiver trabalhando no jQuery, a $.each()função fornecerá uma capacidade semelhante.


Solução ES6: let

O ECMAScript 6 (ES6) introduz novas lete constpalavras - chave cujo escopo é diferente das varvariáveis ​​baseadas em. Por exemplo, em um loop com um letíndice baseado em, cada iteração no loop terá uma nova variável icom escopo de loop, portanto seu código funcionará conforme o esperado. Existem muitos recursos, mas eu recomendaria a publicação de escopo de blocos do 2ality como uma excelente fonte de informações.

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

Porém, lembre-se de que o IE9-IE11 e o Edge anteriores ao Edge 14 suportam leterrado, mas o erro acima não é criado (eles não criam um novo a icada vez); portanto, todas as funções acima registrariam 3 como se usássemos var. O Edge 14 finalmente acerta.

harto
fonte
7
function createfunc(i) { return function() { console.log("My value: " + i); }; }ainda não é fechamento porque usa a variável i?
アレックス
55
Infelizmente, esta resposta está desatualizada e ninguém verá a resposta correta na parte inferior - Function.bind()é definitivamente preferível usar agora, consulte stackoverflow.com/a/19323214/785541 .
Wladimir Palant
81
@ Vladimir: Sua sugestão de que .bind()é "a resposta correta" não está certa. Cada um deles tem seu próprio lugar. Com .bind()você, não é possível vincular argumentos sem vincular o thisvalor. Além disso, você obtém uma cópia do iargumento sem a capacidade de alterá-lo entre as chamadas, o que às vezes é necessário. Portanto, são construções bem diferentes, sem mencionar que as .bind()implementações têm sido historicamente lentas. Claro que no exemplo simples funcionaria, mas os fechamentos são um conceito importante para entender, e era disso que se tratava.
cookie monster
8
Pare de usar esses hacks de função de retorno, use [] .forEach ou [] .map, pois eles evitam reutilizar as mesmas variáveis ​​de escopo.
Christian Landgren
32
@ChristianLandgren: Isso só é útil se você estiver iterando uma matriz. Essas técnicas não são "hacks". Eles são conhecimentos essenciais.
379

Tentar:

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, acho que a resposta mais recente da.bind @ Aust sobre o uso é a melhor maneira de fazer esse tipo de coisa agora. Também há lo-dash / underscore _.partialquando você não precisa ou quer mexer com bind's thisArg.

Bjorn
fonte
2
alguma explicação sobre o }(i));?
aswzen
3
@ Aswzen Eu acho que passa icomo argumento indexpara a função.
Jet Blue
na verdade, ele está criando um índice de variável local.
Abhishek Singh
1
Invoque imediatamente a Expressão de Função, também conhecida como IIFE. (i) é o argumento para a expressão da função anônima que é chamada imediatamente e o índice passa a ser definido a partir de i.
Eggs
348

Outra maneira que ainda não foi mencionada é o uso de Function.prototype.bind

var funcs = {};
for (var i = 0; i < 3; i++) {
  funcs[i] = function(x) {
    console.log('My value: ' + x);
  }.bind(this, i);
}
for (var j = 0; j < 3; j++) {
  funcs[j]();
}

ATUALIZAR

Conforme apontado por @squint e @mekdev, você obtém melhor desempenho criando primeiro a função fora do loop e depois vinculando os resultados ao loop.

function log(x) {
  console.log('My value: ' + x);
}

var funcs = [];

for (var i = 0; i < 3; i++) {
  funcs[i] = log.bind(this, i);
}

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

Aust
fonte
Isto é o que eu faço estes dias também, eu também gosto lo-traço / sublinhado é_.partial
Bjorn
17
.bind()ficará amplamente obsoleto com os recursos do ECMAScript 6. Além disso, isso realmente cria duas funções por iteração. Primeiro o anônimo, depois o gerado por .bind(). Melhor uso seria criá-lo fora do loop e depois .bind()dentro dele.
5
@squint @mekdev - Vocês dois estão corretos. Meu exemplo inicial foi escrito rapidamente para demonstrar como bindé usado. Adicionei outro exemplo por suas sugestões.
Aust
5
Penso que, em vez de desperdiçar a computação em dois O (n) loops, basta fazer para (var i = 0; i <3; i ++) {log.call (this, i); }
user2290820
1
.bind () faz o que a resposta aceita sugere que o PLUS brinca this.
niry
269

Usando uma expressão de função chamada imediatamente , a maneira mais simples e legível de incluir uma variável de índice:

for (var i = 0; i < 3; i++) {

    (function(index) {

        console.log('iterator: ' + index);
        //now you can also loop an ajax call here 
        //without losing track of the iterator value:   $.ajax({});
    
    })(i);

}

Isso envia o iterador ipara a função anônima da qual definimos como index. Isso cria um fechamento, onde a variável ié salva para uso posterior em qualquer funcionalidade assíncrona no IIFE.

neurosnap
fonte
10
Para maior legibilidade do código e para evitar confusão sobre qual ié o quê, renomeio o parâmetro function para index.
precisa saber é o seguinte
5
Como você usaria essa técnica para definir as funções de matriz descritas na pergunta original?
Nico
@Nico Da mesma forma como mostrado na pergunta original, exceto que você usaria em indexvez de i.
JLRishe
@JLRishevar funcs = {}; for (var i = 0; i < 3; i++) { funcs[i] = (function(index) { return function() {console.log('iterator: ' + index);}; })(i); }; for (var j = 0; j < 3; j++) { funcs[j](); }
Nico
1
@Nico No caso particular do OP, eles estão apenas repetindo os números, então isso não seria um bom argumento .forEach(), mas muitas vezes, quando alguém está começando com uma matriz, forEach()é uma boa escolha, como:var nums [4, 6, 7]; var funcs = {}; nums.forEach(function (num, i) { funcs[i] = function () { console.log(num); }; });
JLRishe
164

Um pouco atrasado para a festa, mas eu estava explorando essa questão hoje e notei que muitas das respostas não abordam completamente como o Javascript trata os escopos, que é essencialmente o que isso se resume.

Assim, como muitos outros mencionados, o problema é que a função interna está referenciando a mesma ivariável. Então, por que simplesmente não criamos uma nova variável local a cada iteração e, em vez disso, temos a função interna como referência?

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    var ilocal = i; //create a new local variable
    funcs[i] = function() {
        console.log("My value: " + ilocal); //each should reference its own local variable
    };
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}

Assim como antes, onde cada função interna produziu o último valor atribuído i, agora cada função interna gera apenas o último valor atribuído ilocal. Mas cada iteração não deveria ter a sua ilocal?

Acontece que esse é o problema. Cada iteração está compartilhando o mesmo escopo; portanto, todas as iterações após a primeira são substituídas ilocal. Do MDN :

Importante: JavaScript não tem escopo de bloco. As variáveis ​​introduzidas com um bloco têm o escopo definido para a função ou script que os contém e os efeitos de defini-las persistem além do próprio bloco. Em outras palavras, as instruções de bloco não introduzem um escopo. Embora os blocos "independentes" sejam uma sintaxe válida, você não deseja usar blocos independentes no JavaScript, porque eles não fazem o que você pensa que fazem, se você pensa que eles fazem algo parecido com esses blocos em C ou Java.

Reiterado para enfatizar:

JavaScript não tem escopo de bloco. Variáveis ​​introduzidas com um bloco têm escopo definido para a função ou script que contém

Podemos ver isso verificando ilocalantes de declará-lo em cada iteração:

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
  console.log(ilocal);
  var ilocal = i;
}

É exatamente por isso que esse bug é tão complicado. Mesmo que você esteja redeclarando uma variável, o Javascript não emitirá um erro e o JSLint nem emitirá um aviso. É também por isso que a melhor maneira de resolver isso é tirar proveito dos fechamentos, que é essencialmente a idéia de que, em Javascript, as funções internas têm acesso a variáveis ​​externas porque os escopos internos "incluem" os escopos externos.

Encerramentos

Isso também significa que as funções internas "mantêm" as variáveis ​​externas e as mantêm vivas, mesmo que a função externa retorne. Para utilizar isso, criamos e chamamos uma função wrapper puramente para criar um novo escopo, declarar ilocalno novo escopo e retornar uma função interna que usa ilocal(mais explicações abaixo):

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    funcs[i] = (function() { //create a new scope using a wrapper function
        var ilocal = i; //capture i into a local var
        return function() { //return the inner function
            console.log("My value: " + ilocal);
        };
    })(); //remember to run the wrapper function
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}

Criar a função interna dentro de uma função wrapper fornece à função interna um ambiente privado que somente ela pode acessar, um "fechamento". Assim, toda vez que chamamos a função wrapper, criamos uma nova função interna com seu próprio ambiente separado, garantindo que as ilocalvariáveis ​​não colidem e sobrescrevam umas às outras. Algumas otimizações menores dão a resposta final que muitos outros usuários de SO deram:

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
    funcs[i] = wrapper(i);
}
for (var j = 0; j < 3; j++) {
    funcs[j]();
}
//creates a separate environment for the inner function
function wrapper(ilocal) {
    return function() { //return the inner function
        console.log("My value: " + ilocal);
    };
}

Atualizar

Com o ES6 agora mainstream, agora podemos usar a nova letpalavra-chave para criar variáveis ​​com escopo de bloco:

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (let i = 0; i < 3; i++) { // use "let" to declare "i"
    funcs[i] = function() {
        console.log("My value: " + i); //each should reference its own local variable
    };
}
for (var j = 0; j < 3; j++) { // we can use "var" here without issue
    funcs[j]();
}

Veja como é fácil agora! Para obter mais informações, consulte esta resposta , da qual minhas informações se baseiam.

woojoo666
fonte
Eu gosto de como você explicou a maneira IIFE também. Eu estava procurando por isso. Obrigado.
CapturedTree
4
Agora existe o escopo do bloco no JavaScript usando as palavras let- constchave e . Se essa resposta fosse expandida para incluir isso, seria muito mais útil em termos globais na minha opinião.
@TinyGiant coisa certa, eu adicionei algumas informações sobre lete ligada uma explicação mais completa
woojoo666
@ woojoo666 Sua resposta também poderia funcionar para chamar dois URLs alternativos em um loop como este i=0; while(i < 100) { setTimeout(function(){ window.open("https://www.bbc.com","_self") }, 3000); setTimeout(function(){ window.open("https://www.cnn.com","_self") }, 3000); i++ }:? (poderia substituir window.open () por getelementbyid ......)
nutty about natty
@nuttyaboutnatty desculpe por uma resposta tão tardia. Não parece que o código no seu exemplo já funcione. Você não está usandoi em suas funções de tempo limite, de modo que você não precisa de um fechamento
woojoo666
151

Com o ES6 agora amplamente suportado, a melhor resposta para esta pergunta mudou. O ES6 fornece as palavras let- constchave e para esta circunstância exata. Em vez de brincar com fechamentos, podemos apenas usar letpara definir uma variável de escopo de loop como esta:

var funcs = [];

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

val, em seguida, apontará para um objeto específico para aquela curva específica do loop e retornará o valor correto sem a notação de fechamento adicional. Obviamente, isso simplifica significativamente esse problema.

const é similar a let restrição adicional de que o nome da variável não pode ser recuperado para uma nova referência após a atribuição inicial.

Agora, o suporte ao navegador está disponível para aqueles que têm como alvo as versões mais recentes dos navegadores. const/ letsão atualmente suportados no Firefox, Safari, Edge e Chrome mais recentes. Ele também é suportado no Node, e você pode usá-lo em qualquer lugar, aproveitando as ferramentas de construção como o Babel. Você pode ver um exemplo de trabalho aqui: http://jsfiddle.net/ben336/rbU4t/2/

Documentos aqui:

Porém, lembre-se de que o IE9-IE11 e o Edge anteriores ao Edge 14 suportam leterrado, mas o erro acima não é criado (eles não criam um novo a icada vez); portanto, todas as funções acima registrariam 3 como se usássemos var. O Edge 14 finalmente acerta.

Ben McCormick
fonte
Infelizmente, 'let' ainda não é totalmente suportado, especialmente em dispositivos móveis. developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/…
MattC
2
Em junho de 16, let é suportado em todas as principais versões do navegador, exceto no iOS Safari, Opera Mini e Safari 9. Os navegadores Evergreen o suportam. Babel irá transpilar corretamente para manter o comportamento esperado sem o modo de alta conformidade ativado.
Dan Pantry
@ DanPantry, está na hora de atualizar :) Atualizado para refletir melhor o estado atual das coisas, incluindo a adição de menções const, links de documentos e melhores informações de compatibilidade.
Ben McCormick
Não é por isso que usamos o babel para transpilar nosso código para que navegadores que não suportam o ES6 / 7 possam entender o que está acontecendo?
Pixel 67
87

Outra maneira de dizer é que o iem sua função é vinculado no momento da execução da função, não no momento de criar a função.

Quando você cria o fechamento, ié uma referência à variável definida no escopo externo, não uma cópia dela como era quando você criou o fechamento. Será avaliado no momento da execução.

A maioria das outras respostas fornece maneiras de contornar, criando outra variável que não altera o valor para você.

Apenas pensei em adicionar uma explicação para maior clareza. Para uma solução, pessoalmente, eu iria com a Harto, já que é a maneira mais auto-explicativa de fazê-lo a partir das respostas aqui. Qualquer código postado funcionará, mas eu optaria por uma fábrica de fechamento em vez de ter que escrever uma pilha de comentários para explicar por que estou declarando uma nova variável (Freddy e 1800) ou possuindo uma sintaxe estranha de fechamento incorporado (apphacker).

Darren Clark
fonte
71

O que você precisa entender é que o escopo das variáveis ​​em javascript é baseado na função. Essa é uma diferença importante do que dizer c # onde você tem escopo de bloco e apenas copiar a variável para uma dentro do for funcionará.

Envolvê-lo em uma função que avalia o retorno da função como a resposta do apphacker fará o truque, pois a variável agora tem o escopo da função.

Há também uma palavra-chave let em vez de var, que permitiria o uso da regra de escopo de bloco. Nesse caso, definir uma variável dentro do for faria o truque. Dito isto, a palavra-chave let não é uma solução prática por causa da compatibilidade.

var funcs = {};

for (var i = 0; i < 3; i++) {
  let index = i; //add this
  funcs[i] = function() {
    console.log("My value: " + index); //change to the copy
  };
}

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

eglasius
fonte
@nickf qual navegador? como eu disse, ele tem problemas de compatibilidade, com isso quero dizer problemas sérios de compatibilidade, como eu não acho que o let seja suportado no IE.
eglasius
1
@nickf sim, verificar esta referência: developer.mozilla.org/En/New_in_JavaScript_1.7 ... verifique a seção definições Let, há um exemplo onclick dentro de um loop
eglasius
2
@nickf hmm, na verdade você precisa especificar explicitamente a versão: <script type = "application / javascript; version = 1.7" /> ... Na verdade, eu não o usei em nenhum lugar por causa da restrição do IE, apenas não é prática :(
eglasius
você pode ver o suporte ao navegador para as versões diferentes aqui es.wikipedia.org/wiki/Javascript
eglasius
59

Aqui está outra variação da técnica, semelhante à de Bjorn (apphacker), que permite atribuir o valor da variável dentro da função em vez de passá-lo como um parâmetro, que às vezes pode ser mais claro:

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

Observe que, independentemente da técnica usada, a indexvariável se torna um tipo de variável estática, vinculada à cópia retornada da função interna. Ou seja, alterações em seu valor são preservadas entre as chamadas. Pode ser muito útil.

Boann
fonte
Obrigado e sua solução funciona. Mas eu gostaria de perguntar por que isso funciona, mas trocar a varlinha e a returnlinha não funcionaria? Obrigado!
midnite 3/12/2013
@ midnite Se você trocou vare returna variável não seria atribuída antes de retornar a função interna.
Boann
53

Isso descreve o erro comum de usar fechamentos em JavaScript.

Uma função define um novo ambiente

Considerar:

function makeCounter()
{
  var obj = {counter: 0};
  return {
    inc: function(){obj.counter ++;},
    get: function(){return obj.counter;}
  };
}

counter1 = makeCounter();
counter2 = makeCounter();

counter1.inc();

alert(counter1.get()); // returns 1
alert(counter2.get()); // returns 0

Para cada vez que makeCounteré chamado, {counter: 0}resulta em um novo objeto sendo criado. Além disso, uma nova cópia deobj é criada também para referenciar o novo objeto. Assim, counter1ecounter2 são independentes um do outro.

Fechamentos em loops

Usar um fechamento em um loop é complicado.

Considerar:

var counters = [];

function makeCounters(num)
{
  for (var i = 0; i < num; i++)
  {
    var obj = {counter: 0};
    counters[i] = {
      inc: function(){obj.counter++;},
      get: function(){return obj.counter;}
    }; 
  }
}

makeCounters(2);

counters[0].inc();

alert(counters[0].get()); // returns 1
alert(counters[1].get()); // returns 1

Observe que counters[0]e nãocounters[1] são independentes. De fato, eles operam no mesmoobj !

Isso ocorre porque há apenas uma cópia objcompartilhada em todas as iterações do loop, talvez por razões de desempenho. Mesmo que {counter: 0}crie um novo objeto em cada iteração, a mesma cópia deobj apenas será atualizada com uma referência ao objeto mais novo.

A solução é usar outra função auxiliar:

function makeHelper(obj)
{
  return {
    inc: function(){obj.counter++;},
    get: function(){return obj.counter;}
  }; 
}

function makeCounters(num)
{
  for (var i = 0; i < num; i++)
  {
    var obj = {counter: 0};
    counters[i] = makeHelper(obj);
  }
}

Isso funciona porque variáveis ​​locais no escopo da função diretamente, bem como variáveis ​​de argumento da função, recebem novas cópias na entrada.


fonte
Pequenos esclarecimentos: No primeiro exemplo de Fechos em loops, os contadores [0] e os contadores [1] não são independentes, não por motivos de desempenho. O motivo é que var obj = {counter: 0};é avaliado antes de qualquer código ser executado, conforme declarado em: MDN var : var declarações, onde quer que ocorram, são processadas antes de qualquer código ser executado.
Charidimos 21/09/19
50

A solução mais simples seria,

Ao invés de usar:

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

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

que alerta "2" por 3 vezes. Isso ocorre porque as funções anônimas criadas no loop for compartilham o mesmo fechamento e, nesse fechamento, o valor de ié o mesmo. Use isso para impedir o fechamento compartilhado:

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

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

A idéia por trás disso é encapsular todo o corpo do loop for com uma IIFE (Expressão de Função Imediatamente Invocada) e passar new_icomo parâmetro e capturá-lo como i. Como a função anônima é executada imediatamente, oi valor é diferente para cada função definida dentro da função anônima.

Essa solução parece se adequar a qualquer problema, pois exigirá alterações mínimas no código original que sofrem com esse problema. De fato, isso ocorre por design, não deve ser um problema!

Kemal Dağ
fonte
2
Leia algo semelhante em um livro uma vez. Também prefiro isso, já que você não precisa mais tocar no código existente (e muito) e fica óbvio porque o fez, depois de aprender o padrão da função de auto-chamada: interceptar essa variável no recém-criado escopo.
26613 DanMan
1
@DanMan Thanks. As funções anônimas de chamada automática são uma maneira muito boa de lidar com a falta de escopo da variável no nível do bloco do javascript.
Kemal Dağ
3
Auto-chamada ou auto-chamada não é o termo apropriado para esta técnica, IIFE (Expressão de Função Imediatamente Invocada) é mais precisa. Ref: benalman.com/news/2010/11/...
jherax
31

tente este mais curto

  • sem matriz

  • sem extra para loop


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

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

http://jsfiddle.net/7P6EN/

yilmazburk
fonte
1
Sua solução parece correta, mas desnecessariamente usa funções; por que não apenas console.log a saída? A pergunta original é sobre a criação de funções anônimas que possuem o mesmo fechamento. O problema era que, como eles têm um único fechamento, o valor de i é o mesmo para cada um deles. Espero que você tenha entendido.
precisa
30

Aqui está uma solução simples que usa forEach(trabalha no IE9):

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

Impressões:

My value: 0
My value: 1
My value: 2
Daryl
fonte
27

O principal problema com o código mostrado pelo OP é que inunca é lido até o segundo loop. Para demonstrar, imagine ver um erro dentro do código

funcs[i] = function() {            // and store them in funcs
    throw new Error("test");
    console.log("My value: " + i); // each should log its value.
};

O erro realmente não ocorre até que funcs[someIndex]seja executado (). Usando essa mesma lógica, deve ficar aparente que o valor de itambém não é coletado até este ponto. Uma vez que os acabamentos de ansa originais, i++traz ipara o valor de 3que resulta na condição de i < 3falha e o ciclo termina. Nesse ponto, ié 3e quando funcs[someIndex]()é usado e iavaliado, é 3 - sempre.

Para superar isso, você deve avaliar icomo é encontrado. Observe que isso já aconteceu na forma de funcs[i](onde existem três índices exclusivos). Existem várias maneiras de capturar esse valor. Uma é passá-lo como um parâmetro para uma função que é mostrada de várias maneiras já aqui.

Outra opção é construir um objeto de função que possa fechar sobre a variável. Isso pode ser realizado assim

jsFiddle Demo

funcs[i] = new function() {   
    var closedVariable = i;
    return function(){
        console.log("My value: " + closedVariable); 
    };
};
Travis J
fonte
23

As funções JavaScript "fecham" o escopo ao qual elas têm acesso após a declaração e mantêm o acesso a esse escopo mesmo quando as variáveis ​​nesse escopo são alteradas.

var funcs = []

for (var i = 0; i < 3; i += 1) {
  funcs[i] = function () {
    console.log(i)
  }
}

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

Cada função na matriz acima fecha sobre o escopo global (global, simplesmente porque esse é o escopo em que são declarados).

Posteriormente, essas funções são chamadas registrando o valor mais atual ino escopo global. Essa é a mágica e a frustração do fechamento.

"As funções JavaScript fecham o escopo em que são declaradas e mantêm o acesso a esse escopo, mesmo que os valores das variáveis ​​dentro desse escopo sejam alterados."

O uso em letvez de varresolve isso, criando um novo escopo sempre que o forloop é executado, criando um escopo separado para cada função fechar. Várias outras técnicas fazem a mesma coisa com funções extras.

var funcs = []

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

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

( lettorna o escopo do bloco de variáveis. Os blocos são indicados por chaves, mas no caso do loop for, a variável de inicialização, ino nosso caso, é considerada declarada entre chaves.)

Costa
fonte
1
Eu lutei para entender esse conceito até ler esta resposta. Ele aborda um ponto realmente importante - o valor de iestá sendo definido no escopo global. Quando o forloop termina a execução, o valor global de iagora é 3. Portanto, sempre que essa função é chamada na matriz (usando, digamos funcs[j]), a ifunção nessa referência faz referência à ivariável global (que é 3).
Modermo
13

Após ler várias soluções, gostaria de acrescentar que o motivo pelo qual essas soluções funcionam é confiar no conceito de cadeia de escopo . É a maneira como o JavaScript resolve uma variável durante a execução.

  • Cada definição de função forma um escopo que consiste em todas as variáveis ​​locais declaradas por vare seus arguments.
  • Se tivermos uma função interna definida dentro de outra função (externa), isso formará uma cadeia e será usada durante a execução
  • Quando uma função é executada, o tempo de execução avalia variáveis ​​pesquisando a cadeia de escopo . Se uma variável puder ser encontrada em um determinado ponto da cadeia, ela interromperá a pesquisa e a utilizará; caso contrário, continuará até o escopo global atingido ao qual pertence window.

No código inicial:

funcs = {};
for (var i = 0; i < 3; i++) {         
  funcs[i] = function inner() {        // function inner's scope contains nothing
    console.log("My value: " + i);    
  };
}
console.log(window.i)                  // test value 'i', print 3

Quando funcsé executada, a cadeia de escopo será function inner -> global. Como a variável inão pode ser encontrada em function inner(nem declarada usando varnem passada como argumentos), ela continua pesquisando, até que o valor de iseja encontrado no escopo global que é window.i.

Ao envolvê-lo em uma função externa, defina explicitamente uma função auxiliar como o harto ou use uma função anônima como Bjorn :

funcs = {};
function outer(i) {              // function outer's scope contains 'i'
  return function inner() {      // function inner, closure created
   console.log("My value: " + i);
  };
}
for (var i = 0; i < 3; i++) {
  funcs[i] = outer(i);
}
console.log(window.i)          // print 3 still

Quando funcsé executado, agora a cadeia de escopo será function inner -> function outer. Esse tempo ipode ser encontrado no escopo da função externa, que é executada 3 vezes no loop for, cada vez que o valor é ivinculado corretamente. Não usará o valor de window.iquando executado internamente.

Mais detalhes podem ser encontrados aqui
Inclui o erro comum na criação de fechamento no loop como o que temos aqui, bem como o motivo pelo qual precisamos de fechamento e a consideração do desempenho.

wpding
fonte
Raramente escrevemos esse exemplo de código em real, mas acho que serve um bom exemplo para entender o fundamental. Uma vez que tenhamos o escopo em mente e como eles estão interligados, fica mais claro o porquê de outras maneiras "modernas" Array.prototype.forEach(function callback(el) {})funcionarem naturalmente: o retorno de chamada transmitido naturalmente forma o escopo de empacotamento com el associado corretamente em cada iteração de forEach. Assim, cada função interna definida no retorno de chamada será capaz de usar o direito elvalor
wpding
13

Com os novos recursos do escopo do nível de bloco do ES6, é gerenciado:

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

O código na pergunta do OP é substituído por em letvez de var.

Prithvi Uppalapati
fonte
constfornece o mesmo resultado e deve ser usado quando o valor de uma variável não for alterado. No entanto, o uso de constdentro do inicializador do loop for é implementado incorretamente no Firefox e ainda precisa ser corrigido. Em vez de ser declarado dentro do bloco, ele é declarado fora do bloco, o que resulta em uma redeclaração para a variável, que por sua vez resulta em um erro. O uso de letdentro do inicializador é implementado corretamente no Firefox, por isso não precisa se preocupar.
10

Estou surpreso que ninguém ainda tenha sugerido o uso da forEachfunção para melhor evitar (re) usar variáveis ​​locais. Na verdade, não estou mais usando for(var i ...)por esse motivo.

[0,2,3].forEach(function(i){ console.log('My value:', i); });
// My value: 0
// My value: 2
// My value: 3

// editado para usar em forEachvez do mapa.

Christian Landgren
fonte
3
.forEach()é uma opção muito melhor se você realmente não estiver mapeando nada, e Daryl sugeriu isso sete meses antes da publicação, para que não haja nada para se surpreender.
JLRishe
Esta questão não é sobre loop sobre um array
jherax
Bem, ele quer criar uma matriz de funções, este exemplo mostra como fazer isso sem envolver uma variável global.
Christian Landgren
9

Esta pergunta realmente mostra a história do JavaScript! Agora podemos evitar o escopo do bloco com funções de seta e manipular loops diretamente de nós DOM usando métodos Object.

const funcs = [1, 2, 3].map(i => () => console.log(i));
funcs.map(fn => fn())

const buttons = document.getElementsByTagName("button");
Object
  .keys(buttons)
  .map(i => buttons[i].addEventListener('click', () => console.log(i)));
<button>0</button><br>
<button>1</button><br>
<button>2</button>

sidhuko
fonte
8

A razão pela qual seu exemplo original não funcionou é que todos os fechamentos que você criou no loop referenciaram o mesmo quadro. Com efeito, ter 3 métodos em um objeto com apenas uma única ivariável. Todos eles imprimiram o mesmo valor.

jottos
fonte
8

Primeiro de tudo, entenda o que há de errado com este código:

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

Aqui, quando a funcs[]matriz está sendo inicializada, iestá sendo incrementada, a funcsmatriz é inicializada e o tamanho da funcmatriz se torna 3, então i = 3,. Agora, quando o funcs[j]()é chamado, ele está novamente usando a variáveli , que já foi incrementada para 3.

Agora, para resolver isso, temos muitas opções. Abaixo estão dois deles:

  1. Podemos inicializar icom letou inicializar uma nova variável indexcom lete torná-la igual a i. Portanto, quando a chamada estiver sendo feita, indexela será usada e seu escopo terminará após a inicialização. E para ligar, indexserá inicializado novamente:

    var funcs = [];
    for (var i = 0; i < 3; i++) {          
        let index = i;
        funcs[i] = function() {            
            console.log("My value: " + index); 
        };
    }
    for (var j = 0; j < 3; j++) {
        funcs[j]();                        
    }
  2. Outra opção pode ser a de introduzir um tempFuncque retorne a função real:

    var funcs = [];
    function tempFunc(i){
        return function(){
            console.log("My value: " + i);
        };
    }
    for (var i = 0; i < 3; i++) {  
        funcs[i] = tempFunc(i);                                     
    }
    for (var j = 0; j < 3; j++) {
        funcs[j]();                        
    }
Ali Kahoot
fonte
8

Use a estrutura de fechamento , isso reduziria seu loop for extra. Você pode fazer isso em um único loop for:

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

Vamos verificar o que realmente acontece quando você declara vare let um por um.

Caso1 : usandovar

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

Agora abra a janela do console do Chrome pressionando F12 e atualize a página. Gastar a cada 3 funções dentro da matriz.Você verá uma propriedade chamada [[Scopes]].Expandir essa. Você verá um objeto de matriz chamado "Global", expanda esse. Você encontrará uma propriedade 'i'declarada no objeto com o valor 3.

insira a descrição da imagem aqui

insira a descrição da imagem aqui

Conclusão:

  1. Quando você declara uma variável usando 'var'fora de uma função, ela se torna uma variável global (você pode verificar digitando iou window.ina janela do console. Ele retornará 3).
  2. A função anônima que você declarou não chamará e verificará o valor dentro da função, a menos que você as invoque.
  3. Quando você invoca a função, console.log("My value: " + i)pega o valor do seu Globalobjeto e exibe o resultado.

CASE2: usando let

Agora substitua o 'var'por'let'

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

Faça a mesma coisa, vá para os escopos. Agora você verá dois objetos "Block"e "Global". Agora expanda o Blockobjeto, você verá que 'i' está definido lá, e o estranho é que, para todas as funções, o valor se ifor diferente (0, 1, 2).

insira a descrição da imagem aqui

Conclusão:

Quando você declara uma variável usando 'let'mesmo fora da função, mas dentro do loop, essa variável não será uma variável Global, ela se tornará uma Blockvariável de nível que está disponível apenas para a mesma função. Esse é o motivo, estamos obtendo valores idiferentes para cada função quando invocamos as funções.

Para obter mais detalhes sobre o quão mais próximo funciona, acesse o incrível tutorial em vídeo https://youtu.be/71AtaJpJHw0

Bimal Das
fonte
4

Você pode usar um módulo declarativo para listas de dados como query-js (*). Nessas situações, acho pessoalmente uma abordagem declarativa menos surpreendente

var funcs = Query.range(0,3).each(function(i){
     return  function() {
        console.log("My value: " + i);
    };
});

Você poderia usar seu segundo loop e obter o resultado esperado ou poderia fazer

funcs.iterate(function(f){ f(); });

(*) Eu sou o autor do query-js e, portanto, inclinado a usá-lo, portanto, não tome minhas palavras como uma recomendação para a referida biblioteca apenas para a abordagem declarativa :)

Rune FS
fonte
1
Eu adoraria uma explicação sobre o voto negativo. O código resolve o problema em questão. Seria útil saber como potencialmente melhorar o código
Rune FS
1
O que é Query.range(0,3)? Isso não faz parte das tags desta pergunta. Além disso, se você usar uma biblioteca de terceiros, poderá fornecer o link da documentação.
jherax
1
@ jaxax esses são ou obviamente melhorias óbvias. Obrigado pelo comentário. Eu poderia jurar que já havia um link. Com isso, o post foi bem inútil, eu acho :). Minha ideia inicial de mantê-lo fora era porque eu não estava tentando forçar o uso da minha própria biblioteca, mas mais a ideia declarativa. No entanto, em hinsight Concordo plenamente que o link deve estar lá
Rune FS
4

Eu prefiro usar a forEachfunção, que tem seu próprio fechamento com a criação de um pseudo intervalo:

var funcs = [];

new Array(3).fill(0).forEach(function (_, i) { // creating a range
    funcs[i] = function() {            
        // now i is safely incapsulated 
        console.log("My value: " + i);
    };
});

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

Isso parece mais feio do que em outros idiomas, mas IMHO é menos monstruoso que outras soluções.

Rax Wunter
fonte
Prefere a quê? Este parece ser um comentário em resposta a outra resposta. Ele não aborda a questão real (já que você não está atribuindo uma função, para ser chamada posteriormente, em qualquer lugar).
Quentin
É relacionado exatamente à citada questão: como fazer uma iteração com segurança, sem problemas de fechamento
Rax Wunter
Agora, não parece significativamente diferente da resposta aceita.
Quentin
Não. Na resposta aceita, é sugerido o uso de "alguma matriz", mas lidamos com um intervalo na resposta, são coisas absolutamente diferentes, que infelizmente não têm uma boa solução em js, então minha resposta está tentando resolver a questão de uma maneira boa e prática
Rax Wunter
@Quentin Eu recomendaria investigar a solução antes de fazer uma recusa
Rax Wunter
4

E ainda outra solução: em vez de criar outro loop, basta vincular a thisfunção de retorno.

var funcs = [];

function createFunc(i) {
  return function() {
    console.log('My value: ' + i); //log value of i.
  }.call(this);
}

for (var i = 1; i <= 5; i++) {  //5 functions
  funcs[i] = createFunc(i);     // call createFunc() i=5 times
}

Ao vincular isso , resolve o problema também.

pixel 67
fonte
3

Muitas soluções parecem corretas, mas não mencionam que é chamado de Curryingpadrão de design de programação funcional para situações como aqui. 3-10 vezes mais rápido que o bind, dependendo do navegador.

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

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

Veja o ganho de desempenho em diferentes navegadores .

Pawel
fonte
@TinyGiant O exemplo com a função retornada ainda está otimizando o desempenho. Eu não pularia em funções de seta como todos os blogueiros de JavaScript. Eles parecem legais e limpos, mas promovem funções de escrita em linha, em vez de usar funções predefinidas. Isso pode ser uma armadilha não óbvia em locais quentes. Outro problema é que eles não são apenas açúcar sintático porque estão executando ligações desnecessárias, criando fechamentos de embalagem.
Pawel
2
Aviso para futuros leitores: Esta resposta aplica incorretamente o termo Caril . "Currying é quando você divide uma função que leva vários argumentos em uma série de funções que fazem parte dos argumentos." . Este código não faz nada do tipo. Tudo o que você fez aqui foi pegar o código da resposta aceita, mudar algumas coisas, mudar o estilo e nomear um pouco e chamar de currying, o que categoricamente não é.
3

Seu código não funciona, porque o que faz é:

Create variable `funcs` and assign it an empty array;  
Loop from 0 up until it is less than 3 and assign it to variable `i`;
    Push to variable `funcs` next function:  
        // Only push (save), but don't execute
        **Write to console current value of variable `i`;**

// First loop has ended, i = 3;

Loop from 0 up until it is less than 3 and assign it to variable `j`;
    Call `j`-th function from variable `funcs`:  
        **Write to console current value of variable `i`;**  
        // Ask yourself NOW! What is the value of i?

Agora, a pergunta é: qual é o valor da variável iquando a função é chamada? Como o primeiro loop é criado com a condição de i < 3, ele para imediatamente quando a condição é falsa, por isso é i = 3.

Você precisa entender que, no momento em que suas funções são criadas, nenhum código é executado, ele é salvo apenas para mais tarde. E assim, quando são chamados mais tarde, o intérprete os executa e pergunta: "Qual é o valor atual i?"

Portanto, seu objetivo é primeiro salvar o valor de ito function e somente depois salvar a função em funcs. Isso pode ser feito, por exemplo, desta maneira:

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

Dessa forma, cada função terá sua própria variável xe configuramos isso xpara o valor de iem cada iteração.

Essa é apenas uma das várias maneiras de resolver esse problema.

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

Use let (escopo bloqueado) em vez de var.

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

BG Hari Prasad
fonte