Domanda Chiusura JavaScript all'interno di loop - semplice esempio pratico


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
}

Emette questo:

Il mio valore: 3
  Il mio valore: 3
  Il mio valore: 3

Mentre mi piacerebbe che uscisse:

Il mio valore: 0
  Il mio valore: 1
  Il mio valore: 2


Lo stesso problema si verifica quando il ritardo nell'esecuzione della funzione è causato dall'utilizzo di listener di eventi:

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>

... o codice asincrono, ad es. 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 è la soluzione a questo problema di base?


2316
2018-04-15 06:06


origine


risposte:


Bene, il problema è che la variabile i, all'interno di ciascuna delle tue funzioni anonime, è associato alla stessa variabile al di fuori della funzione.

Soluzione classica: chiusure

Quello che vuoi fare è legare la variabile all'interno di ogni funzione a un valore separato, immutabile al di fuori della funzione:

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
}

Poiché non esiste un ambito di blocco in JavaScript - solo ambito di funzione - avvolgendo la creazione della funzione in una nuova funzione, ci si assicura che il valore di "i" rimanga come desiderato.


Soluzione 2015: per tutti

Con la disponibilità relativamente diffusa del Array.prototype.forEach funzione (nel 2015), vale la pena notare che in quelle situazioni che coinvolgono l'iterazione principalmente su una serie di valori, .forEach() fornisce un modo pulito e naturale per ottenere una chiusura distinta per ogni iterazione. Cioè, supponendo che tu abbia una sorta di array contenente valori (riferimenti DOM, oggetti, qualunque cosa) e si pone il problema di impostare i callback specifici per ciascun elemento, puoi farlo:

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

L'idea è che ogni invocazione della funzione di callback utilizzata con .forEach il ciclo sarà la sua chiusura. Il parametro passato a quel gestore è l'elemento dell'array specifico per quel particolare passaggio dell'iterazione. Se viene utilizzato in una richiamata asincrona, non si scontrerà con nessuno degli altri callback stabiliti in altri passaggi dell'iterazione.

Se lavori in jQuery, il $.each() funzione ti dà una capacità simile.


Soluzione ES6: let

ECMAScript 6 (ES6), la versione più recente di JavaScript, sta ora iniziando a essere implementata in molti browser e sistemi di backend sempreverdi. Ci sono anche transpilers come Babele questo convertirà ES6 in ES5 per consentire l'utilizzo di nuove funzionalità su sistemi precedenti.

ES6 introduce nuovo let e const parole chiave con un ambito diverso da varvariabili basate su Ad esempio, in un ciclo con a letbasato su indice, ogni iterazione attraverso il ciclo avrà un nuovo valore di i dove ogni valore è spaziato all'interno del ciclo, quindi il tuo codice funzionerà come previsto. Ci sono molte risorse, ma lo raccomanderei Post di scoping del blocco di 2ality come una grande fonte di informazioni.

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

Attenzione, tuttavia, che IE9-IE11 e Edge precedenti al supporto per Edge 14 let ma ottenere il sopra sbagliato (non creano un nuovo i ogni volta, quindi tutte le funzioni di cui sopra registreranno 3 come se avessero usato var). Edge 14 finalmente ha ragione.


1794
2018-04-15 06:18



Provare:

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

modificare (2014):

Personalmente penso @ Aust risposta più recente sull'utilizzo .bind è il modo migliore per fare questo genere di cose ora. C'è anche lo-dash / underscore's _.partial quando non hai bisogno o vuoi fare casino bind'S thisArg.


340
2018-04-15 06:10



Un altro modo che non è stato ancora menzionato è l'uso di 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]();
}

AGGIORNARE

Come sottolineato da @squint e @mekdev, si ottengono prestazioni migliori creando prima la funzione all'esterno del ciclo e quindi vincolando i risultati all'interno del ciclo.

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


310
2017-10-11 16:41



Utilizzando un Espressione funzione invocata immediatamente, il modo più semplice e più leggibile per racchiudere una variabile di indice:

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

}

Questo invia l'iteratore i nella funzione anonima di cui definiamo come index. Questo crea una chiusura, dove la variabile i viene salvato per un utilizzo successivo in qualsiasi funzionalità asincrona all'interno dell'IIFE.


234
2017-10-11 18:23



Un po 'tardi per la festa, ma stavo esplorando questo problema oggi e ho notato che molte delle risposte non affrontano completamente il modo in cui Javascript tratta gli ambiti, che è essenzialmente ciò a cui questo si riduce.

Così come molti altri hanno menzionato, il problema è che la funzione interiore fa riferimento allo stesso modo i variabile. Quindi, perché non creiamo una nuova variabile locale ogni iterazione e abbiamo invece la funzione interna di riferimento?

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

Proprio come prima, dove ogni funzione interna ha emesso l'ultimo valore assegnato a i, ora ogni funzione interna emette solo l'ultimo valore assegnato a ilocal. Ma non dovrebbe ogni iterazione avere il proprio ilocal?

Risulta, questo è il problema. Ogni iterazione condivide lo stesso ambito, quindi ogni iterazione dopo la prima è solo sovrascrittura ilocal. A partire dal MDN:

Importante: JavaScript non ha scope di blocco. Le variabili introdotte con un blocco hanno come ambito la funzione o lo script di contenimento e gli effetti dell'impostazione persistono oltre il blocco stesso. In altre parole, le istruzioni di blocco non introducono un ambito. Anche se i blocchi "standalone" sono una sintassi valida, non si desidera utilizzare blocchi autonomi in JavaScript, perché non fanno ciò che si pensa di fare, se si pensa che facciano qualcosa come questi blocchi in C o Java.

Reiterato per enfasi:

JavaScript non ha scope di blocco. Le variabili introdotte con un blocco hanno come ambito la funzione o lo script di contenimento

Possiamo vedere questo controllando ilocal prima di dichiararlo in ogni iterazione:

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

Questo è esattamente il motivo per cui questo bug è così complicato. Anche se stai redecando una variabile, Javascript non genererà un errore e JSLint non invierà nemmeno un avviso. Questo è anche il motivo per cui il modo migliore per risolvere questo è sfruttare le chiusure, che è essenzialmente l'idea che in Javascript, le funzioni interne abbiano accesso alle variabili esterne perché gli ambiti interni "racchiudono" gli ambiti esterni.

Closures

Ciò significa anche che le funzioni interne "tengono" le variabili esterne e le mantengono in vita, anche se la funzione esterna ritorna. Per utilizzare questo, creiamo e chiamiamo una funzione wrapper puramente per creare un nuovo ambito, dichiarare ilocalnel nuovo ambito e restituisce una funzione interna che utilizza ilocal (ulteriori spiegazioni di seguito):

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

La creazione della funzione interna all'interno di una funzione wrapper conferisce alla funzione interiore un ambiente privato a cui solo esso può accedere, una "chiusura". Quindi, ogni volta che chiamiamo la funzione wrapper creiamo una nuova funzione interna con il suo ambiente separato, assicurando che il ilocal le variabili non si scontrano e si sovrascrivono a vicenda. Alcune ottimizzazioni minori forniscono la risposta definitiva che molti altri utenti SO hanno dato:

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

Aggiornare

Con ES6 ora mainstream, ora possiamo usare il nuovo let parola chiave per creare variabili con scope a blocchi:

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

Guarda com'è facile ora! Per ulteriori informazioni, vedere questa risposta, su cui si basano le mie informazioni.


127
2018-04-10 09:57



Con ES6 ora ampiamente supportato, la migliore risposta a questa domanda è cambiata. ES6 fornisce il let e const parole chiave per questa esatta circostanza. Invece di scherzare con chiusure, possiamo semplicemente usare let per impostare una variabile del ciclo di variazione come questa:

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

val indicherà quindi un oggetto specifico per quel particolare giro del ciclo e restituirà il valore corretto senza la notazione di chiusura aggiuntiva. Questo ovviamente semplifica notevolmente questo problema.

const è simile a let con la restrizione aggiuntiva che il nome della variabile non può essere rimbalzato su un nuovo riferimento dopo l'assegnazione iniziale.

Il supporto del browser è ora disponibile per coloro che scelgono come target le ultime versioni dei browser. const/let sono attualmente supportati negli ultimi Firefox, Safari, Edge e Chrome. Inoltre è supportato in Nodo, e puoi usarlo ovunque sfruttando strumenti di costruzione come Babel. Puoi vedere un esempio di lavoro qui: http://jsfiddle.net/ben336/rbU4t/2/

Docs qui:

Attenzione, tuttavia, che IE9-IE11 e Edge precedenti al supporto per Edge 14 let ma ottenere il sopra sbagliato (non creano un nuovo i ogni volta, quindi tutte le funzioni di cui sopra registreranno 3 come se avessero usato var). Edge 14 finalmente ha ragione.


121
2018-05-21 03:04



Un altro modo per dirlo è che il i nella tua funzione è vincolato al momento dell'esecuzione della funzione, non il tempo di creazione della funzione.

Quando crei la chiusura, i è un riferimento alla variabile definita nell'ambito esterno, non una copia di esso come era quando hai creato la chiusura. Sarà valutato al momento dell'esecuzione.

La maggior parte delle altre risposte fornisce dei modi per aggirare creando un'altra variabile che non cambierà il valore per te.

Ho pensato di aggiungere una spiegazione per chiarezza. Per una soluzione, personalmente, andrei con Harto's poiché è il modo più intuitivo di farlo dalle risposte qui. Qualunque codice pubblicato funzionerà, ma opterei per un factory di chiusura per dover scrivere una pila di commenti per spiegare perché sto dichiarando una nuova variabile (Freddy e 1800) o ho una strana sintassi di chiusura incorporata (apphacker).


77
2018-04-15 06:48