for - Fecho de JavaScript dentro de loops-exemplo prático simples




javascript loop i++ (20)

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

Caso 1 : 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 sua 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 que possui valor 3.

Conclusão:

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

CASE2: usando let

Agora substitua o 'var'com'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 Blockobjeto, você verá que 'i' é definido lá, e o estranho é que, para cada função, o valor ié diferente (0, 1, 2).

Conclusão:

Quando você declara a variável usando 'let'até mesmo fora da função, mas dentro do loop, esta 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. Essa é a razão pela qual estamos obtendo valor de idiferentes para cada função quando invocamos as funções.

Para mais detalhes sobre como funciona mais de perto, por favor, vá até o incrível vídeo tutorial https://youtu.be/71AtaJpJHw0

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
}

Isso gera isso:

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

Considerando que eu gostaria de produzir:

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 ouvintes de evento:

var buttons = document.getElementsByTagName("button");
for (var i = 0; i < buttons.length; i++) {          // let's create 3 functions
  buttons[i].addEventListener("click", function() { // as event listeners
    console.log("My value: " + i);                  // each should log its value.
  });
}
<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++){
  wait(i * 100).then(() => console.log(i)); // Log `i` as soon as each promise resolves.
}

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


tente este mais curto

  • nenhuma 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/


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á-la como um parâmetro, o que pode ser mais claro às vezes:

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

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


Aqui está uma solução simples que usa forEach (funciona de volta ao 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

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

Solução clássica: Closures

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++) {
    funcs[j]();                        // and now let's run each one to see
}

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

Solução 2015: forEach

Com a disponibilidade relativamente ampla da função Array.prototype.forEach (em 2015), é importante notar que, nessas situações que envolvem iteração principalmente sobre uma matriz de valores, .forEach() fornece uma maneira natural e limpa de obter um fechamento distinto para cada iteração. Ou seja, supondo que você tenha algum tipo de array contendo valores (referências DOM, objetos, o que quer que seja), e o problema surge 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 ideia é que cada chamada da função de retorno de chamada usada com o loop .forEach será 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 função $.each() lhe dará uma capacidade semelhante.

Solução ES6: let

O ECMAScript 6 (ES6), a mais nova versão do JavaScript, está agora começando a ser implementado em muitos navegadores e sistemas back-end. Também existem transpiladores como o Babel que convertem o ES6 para o ES5 para permitir o uso de novos recursos em sistemas mais antigos.

O ES6 introduz novas palavras-chave let e const escopo diferente das variáveis ​​baseadas em var . Por exemplo, em um loop com um índice baseado em let , cada iteração através do loop terá um novo valor de i qual cada valor é delimitado dentro do loop, para que seu código funcione conforme o esperado. Existem muitos recursos, mas eu recomendo o post de escopo de bloco do 2ality como uma ótima fonte de informações.

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, mas obtenham o erro acima (eles não criam um novo i cada vez, então todas as funções acima registrariam 3 como se var ). O Edge 14 finalmente acerta.


Com o ES6 agora amplamente suportado, a melhor resposta para essa pergunta mudou. O ES6 fornece as palavras-chave let e const para essa circunstância exata. Em vez de mexer com closures, podemos usar let para 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 que é específico para essa volta específica do loop e retornará o valor correto sem a notação de fechamento adicional. Isso obviamente simplifica significativamente esse problema.

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

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

Docs aqui:

Cuidado, porém, que o IE9-IE11 e o Edge antes do Edge 14 suportam, mas obtenham o erro acima (eles não criam um novo i cada vez, então todas as funções acima registrariam 3 como se var ). O Edge 14 finalmente acerta.


Isso descreve o erro comum de usar closures 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 é invocado, {counter: 0} resulta em um novo objeto sendo criado. Além disso, uma nova cópia do obj é criada para referenciar o novo objeto. Assim, counter1 e counter2 são independentes um do outro.

Fechamentos em loops

Usando um encerramento 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 os counters[0] e counters[1] não são independentes. Na verdade, eles operam no mesmo obj !

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

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, assim como variáveis ​​de argumento de função, são alocadas novas cópias na entrada.

Para uma discussão detalhada, veja as armadilhas e o uso do fechamento do JavaScript


O principal problema com o código mostrado pelo OP é que i nunca é 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 claro que o valor de i também não é coletado até este ponto. Uma vez que o loop original termina, o i++ traz i para o valor de 3 que resulta na condição i < 3 falha e no final do loop. Neste ponto, i é 3 e assim quando funcs[someIndex]() é usado, e i é avaliado, é 3 - toda vez.

Para superar isso, você deve avaliar i como é encontrado. Note que isso já aconteceu na forma de funcs[i] (onde existem 3 índices exclusivos). Existem várias maneiras de capturar esse valor. Uma é passá-la 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 será capaz de fechar a variável. Isso pode ser feito assim

jsFiddle Demo

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

Outra maneira de dizer isso é que o i em sua função está ligado no momento de executar a função, não o tempo 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 altere o valor para você.

Apenas pensei em adicionar uma explicação para clareza. Para uma solução, pessoalmente, eu usaria o Harto, já que é a maneira mais auto-explicativa de fazer isso a partir das respostas aqui. Qualquer um dos códigos postados funcionará, mas eu optaria por uma fábrica de fechamento por ter que escrever uma pilha de comentários para explicar por que estou declarando uma nova variável (Freddy e 1800) ou ter uma sintaxe de fechamento embutida estranha (apphacker).


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

Como apontado por @squint e @mekdev, você obtém um melhor desempenho criando primeiro a função fora do loop e vinculando os resultados dentro do 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]();
}


Usando uma Expressão de Função Invocada 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 i para a função anônima que definimos como index . Isso cria um fechamento, onde a variável i é salva para uso posterior em qualquer funcionalidade assíncrona dentro do IIFE.


CONTRA SER PRIMITIVO

Vamos definir funções de retorno de chamada da seguinte maneira:

// ****************************
// COUNTER BEING A PRIMITIVE
// ****************************
function test1() {
    for (var i=0; i<2; i++) {
        setTimeout(function() {
            console.log(i);
        });
    }
}
test1();
// 2
// 2

Depois que o tempo limite terminar, ele imprimirá 2 para ambos. Isso ocorre porque a função de retorno de chamada acessa o valor com base no escopo léxico , onde a função foi definida.

Para passar e preservar o valor enquanto o retorno de chamada foi definido, podemos criar um closure , para preservar o valor antes que o retorno de chamada seja chamado. Isso pode ser feito da seguinte forma:

function test2() {
    function sendRequest(i) {
        setTimeout(function() {
            console.log(i);
        });
    }

    for (var i = 0; i < 2; i++) {
        sendRequest(i);
    }
}
test2();
// 1
// 2

Agora, o que é especial sobre isso é "As primitivas são passadas por valor e copiadas. Assim, quando o fechamento é definido, elas mantêm o valor do loop anterior."

CONTRA UM OBJETO

Como os closures têm acesso às variáveis ​​de função pai por referência, essa abordagem seria diferente daquela das primitivas.

// ****************************
// COUNTER BEING AN OBJECT
// ****************************
function test3() {
    var index = { i: 0 };
    for (index.i=0; index.i<2; index.i++) {
        setTimeout(function() {
            console.log('test3: ' + index.i);
        });
    }
}
test3();
// 2
// 2

Portanto, mesmo se um fechamento for criado para a variável que está sendo passada como um objeto, o valor do índice de loop não será preservado. Isso é para mostrar que os valores de um objeto não são copiados enquanto são acessados ​​por referência.

function test4() {
    var index = { i: 0 };
    function sendRequest(index, i) {
        setTimeout(function() {
            console.log('index: ' + index);
            console.log('i: ' + i);
            console.log(index[i]);
        });
    }

    for (index.i=0; index.i<2; index.i++) {
        sendRequest(index, index.i);
    }
}
test4();
// index: { i: 2}
// 0
// undefined

// index: { i: 2}
// 1
// undefined

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 torna-se 3, portanto i = 3,. Agora, quando o funcs[j]()é chamado, ele está novamente usando a variável i, 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 chamar, 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 retorna 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]();                        
    }
    

Seu código não funciona, porque o que ele 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 questão é, qual é o valor da variável iquando a função é chamada? Como o primeiro loop é criado com a condição de i < 3, ele pára imediatamente quando a condição é falsa, portanto é 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 de i?"

Assim, seu objetivo é primeiro salvar o valor de ipara funcionar e somente depois disso salvar a função para 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
}

Desta forma, cada função terá sua própria variável xe nós configuramos isto xpara o valor de icada iteração.

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


Depois de ler várias soluções, gostaria de acrescentar que o motivo pelo qual essas soluções funcionam é confiar no conceito de cadeia de escopo . É o modo 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 suas arguments.
  • Se tivermos uma função interna definida dentro de outra função (externa), isso forma 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 será interrompida na pesquisa e no uso, caso contrário, continuará até que o escopo global seja 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 funcsexecutado, 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 a procurar, até que o valor iseja finalmente encontrado no escopo global que é window.i.

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

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 funcsexecutado, agora a cadeia de escopo será function inner -> function outer. Este tempo ipode ser encontrado no escopo da função externa, que é executado 3 vezes no loop for, cada vez que o valor é iligado corretamente. Não usará o valor de window.iquando executado internamente.

Mais detalhes podem ser encontrados here
Ele inclui o erro comum na criação de fechamento no loop como o que temos aqui, bem como por que precisamos de fechamento e consideração de desempenho.


E ainda outra solução: em vez de criar outro loop, apenas ligue a thisfunção return.

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
}

Ligando isso, resolve o problema também.


Estou surpreso que ninguém ainda tenha sugerido usar a forEachfunção para evitar melhor (re) usar variáveis ​​locais. Na verdade, não estou mais usando for(var i ...)nada 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 de mapear.


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 os outros idiomas, mas IMHO é menos monstruoso que outras soluções.


Use estrutura de closure , isso reduziria o seu loop extra. Você pode fazer isso em um único loop:

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

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

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

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

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

(*) Sou autor de query-js e por isso inclinado a usá-lo, então não tome minhas palavras como recomendação para a dita biblioteca apenas para a abordagem declarativa :)







closures