Chiusura JavaScript all'interno dei loop: semplice esempio pratico


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

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 dei listener di eventi:

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>

... o codice asincrono, ad esempio utilizzando 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));
}

Modifica: è anche evidente in for ine for ofloop:

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 è la soluzione a questo problema di base?


55
Sei sicuro di non voler funcsessere un array, se stai utilizzando indici numerici? Solo un avviso.
DanMan,

23
Questo è un problema davvero confuso. Questo articolo mi aiuta a capirlo . Potrebbe aiutare anche gli altri.
user3199690,

4
Un'altra soluzione semplice e spiegata: 1) Le funzioni nidificate hanno accesso all'ambito "sopra" loro ; 2) una soluzione di chiusura ... "Una chiusura è una funzione che ha accesso all'ambito padre, anche dopo la chiusura della funzione padre".
Peter Krauss,

2
Fai riferimento a questo link per una migliore comprensione javascript.info/tutorial/advanced-functions
Saurabh Ahuja

35
In ES6 , una soluzione banale è dichiarare la variabile i con let , che è mirata al corpo del loop.
Tomas Nikodym,

Risposte:


2149

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

Soluzione classica: chiusure

Quello che vuoi fare è associare la variabile all'interno di ciascuna funzione a un valore separato e invariato 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++) {
  // and now let's run each one to see
  funcs[j]();
}

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


Soluzione ES5.1: forOach

Con la disponibilità relativamente diffusa della Array.prototype.forEachfunzione (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 che sorga il problema di impostare callback specifici per ogni 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 usata con il .forEachloop sarà la sua propria chiusura. Il parametro passato a quel gestore è l'elemento array specifico per quel particolare passaggio dell'iterazione. Se viene utilizzato in un callback asincrono, non si scontrerà con nessuno degli altri callback stabiliti in altri passaggi dell'iterazione.

Se ti capita di lavorare in jQuery, la $.each()funzione ti offre una funzionalità simile.


Soluzione ES6: let

ECMAScript 6 (ES6) introduce nuovi lete constparole chiave con ambito diverso rispetto alle varvariabili basate. Ad esempio, in un ciclo con un letindice basato su, ogni iterazione attraverso il ciclo avrà una nuova variabile icon ambito del ciclo, quindi il codice funzionerà come previsto. Ci sono molte risorse, ma raccomanderei il post di scoping a blocchi di 2ality come un'ottima fonte di informazioni.

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

Attenzione, però, che IE9-IE11 e Edge prima del Edge 14 supportano letma sbagliano quanto sopra (non ne creano una nuova iogni volta, quindi tutte le funzioni sopra registrerebbero 3 come farebbero se lo usassimo var). Edge 14 ha finalmente capito bene.


7
non è function createfunc(i) { return function() { console.log("My value: " + i); }; }ancora chiusura perché utilizza la variabile i?
ア レ ッ ク ス

55
Sfortunatamente, questa risposta è obsoleta e nessuno vedrà la risposta corretta in fondo - l'utilizzo Function.bind()è decisamente preferibile ormai, vedi stackoverflow.com/a/19323214/785541 .
Wladimir Palant,

81
@Wladimir: il tuo suggerimento che .bind()è "la risposta corretta" non è giusto. Ognuno di loro ha il suo posto. Con .bind()te non puoi associare argomenti senza associare il thisvalore. Inoltre ottieni una copia idell'argomento senza la possibilità di mutarlo tra le chiamate, che a volte è necessario. Quindi sono costrutti abbastanza diversi, per non parlare del fatto che le .bind()implementazioni sono state storicamente lente. Certo, nel semplice esempio funzionerebbe, ma le chiusure sono un concetto importante da capire, ed è di questo che si trattava.
cookie monster

8
Smetti di usare questi hack di funzione for-return, usa invece [] .forEach o [] .map perché evitano di riutilizzare le stesse variabili dell'ambito.
Christian Landgren,

32
@ChristianLandgren: è utile solo se stai ripetendo una matrice. Queste tecniche non sono "hack". Sono conoscenze essenziali.

379

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

Modifica (2014):

Personalmente penso che la risposta più recente di.bind @ Aust sull'utilizzo sia il modo migliore per fare questo tipo di cose ora. C'è anche lo-dash / underscore _.partialquando non hai bisogno o vuoi fare casino con quelli binddi thisArg.


2
qualche spiegazione sul }(i));?
aswzen

3
@aswzen Penso che passi icome argomento indexalla funzione.
Jet Blue,

sta effettivamente creando un indice variabile locale.
Abhishek Singh,

1
Richiama immediatamente Function Expression, aka IIFE. (i) è l'argomento per l'espressione della funzione anonima che viene richiamato immediatamente e l'indice viene impostato da i.
Uova

348

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


Questo è quello che faccio anche in questi giorni, mi piace anche lo-dash / underscore_.partial
Bjorn

17
.bind()sarà in gran parte obsoleto con le funzionalità di ECMAScript 6. Inoltre, questo in realtà crea due funzioni per iterazione. Prima l'anonimo, poi quello generato da .bind(). Un uso migliore sarebbe crearlo al di fuori del ciclo, quindi .bind()all'interno.

5
@squint @mekdev - Entrambi avete ragione. Il mio esempio iniziale è stato scritto rapidamente per dimostrare come bindviene utilizzato. Ho aggiunto un altro esempio per i tuoi suggerimenti.
Agosto

5
Penso invece di sprecare il calcolo su due loop O (n), basta fare per (var i = 0; i <3; i ++) {log.call (this, i); }
user2290820

1
.bind () fa quello con cui la risposta accettata suggerisce che giocherà con PLUS this.
niry

269

Utilizzando un'espressione di funzione richiamata immediatamente , il modo più semplice e leggibile per racchiudere una variabile 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 inella funzione anonima di cui definiamo index. Questo crea una chiusura, in cui la variabile iviene salvata per un uso successivo in qualsiasi funzionalità asincrona all'interno di IIFE.


10
Per ulteriore leggibilità del codice ed evitare confusione su quale isia la cosa, rinominerei il parametro della funzione in index.
Kyle Falconer,

5
Come useresti questa tecnica per definire le funzioni dell'array descritte nella domanda originale?
Nico,

@Nico Come mostrato nella domanda originale, tranne che tu useresti indexinvece di 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 Nel caso particolare di OP, stanno solo ripetendo i numeri, quindi questo non sarebbe un ottimo caso .forEach(), ma molte volte, quando si inizia con un array, forEach()è una buona scelta, come:var nums [4, 6, 7]; var funcs = {}; nums.forEach(function (num, i) { funcs[i] = function () { console.log(num); }; });
JLRishe

164

Un po 'tardi alla 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 si riduce.

Quindi, come molti altri hanno menzionato, il problema è che la funzione interna fa riferimento alla stessa ivariabile. Quindi perché non creare una nuova variabile locale ogni iterazione e fare riferimento invece alla funzione interna?

//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 i, ora ogni funzione interna emette solo l'ultimo valore assegnato a ilocal. Ma ogni iterazione non dovrebbe avere la sua ilocal?

Si scopre, questo è il problema. Ogni iterazione condivide lo stesso ambito, quindi ogni iterazione dopo la prima è solo sovrascrittura ilocal. Da MDN :

Importante: JavaScript non ha ambito di blocco. Le variabili introdotte con un blocco sono incluse nella funzione o nello script di contenimento e gli effetti dell'impostazione persistono oltre il blocco stesso. In altre parole, le istruzioni di blocco non introducono un ambito. Sebbene i blocchi "autonomi" siano una sintassi valida, non si desidera utilizzare blocchi autonomi in JavaScript, perché non fanno ciò che si ritiene facciano, se si ritiene che facciano qualcosa del genere in C o Java.

Ribadito per enfasi:

JavaScript non ha ambito di blocco. Le variabili introdotte con un blocco sono incluse nella funzione o nello script di contenimento

Possiamo vederlo controllando ilocalprima 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 dichiarando una variabile, Javascript non genererà un errore e JSLint non emetterà nemmeno un avviso. Questo è anche il motivo per cui il modo migliore per risolverlo è 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.

chiusure

Questo significa anche che le funzioni interne "si aggrappano" alle variabili esterne e le mantengono in vita, anche se la funzione esterna ritorna. Per utilizzarlo, creiamo e chiamiamo una funzione wrapper esclusivamente per creare un nuovo ambito, dichiarare ilocalnel nuovo ambito e restituire 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 interna un ambiente privato a cui solo essa può accedere, una "chiusura". Pertanto, ogni volta che chiamiamo la funzione wrapper creiamo una nuova funzione interna con il suo ambiente separato, assicurando che le ilocalvariabili non si scontrino e si sovrascrivano. Alcune piccole ottimizzazioni danno la risposta finale 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 ormai mainstream, ora possiamo usare la nuova letparola chiave per creare variabili con ambito di blocco:

//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 adesso! Per ulteriori informazioni, consultare questa risposta , su cui si basano le mie informazioni.


Mi piace come hai spiegato anche il modo IIFE. Lo stavo cercando. Grazie.
CapturedTree

4
Ora esiste qualcosa come l'ambito del blocco in JavaScript usando le parole chiave lete const. Se questa risposta si espandesse per includerla, secondo me sarebbe molto più utile a livello globale.

@TinyGiant, ho aggiunto alcune informazioni lete ho collegato una spiegazione più completa
woojoo666

@ woojoo666 Potrebbe la risposta anche il lavoro per chiamare due URL alternato è in un ciclo in questo modo: 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++ }? (potrebbe sostituire window.open () con getelementbyid ......)
nocciola circa natty

@nuttyaboutnatty mi dispiace per una risposta così in ritardo. Non sembra che il codice nel tuo esempio funzioni già. Non stai utilizzando le itue funzioni di timeout, quindi non hai bisogno di una chiusura
woojoo666,

151

Con ES6 ora ampiamente supportato, la migliore risposta a questa domanda è cambiata. ES6 fornisce le parole chiave lete constper questa circostanza esatta. Invece di fare casini con le chiusure, possiamo semplicemente usare letper impostare una variabile dell'ambito del ciclo come questa:

var funcs = [];

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

valindicherà 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 alla letrestrizione 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 hanno come target le ultime versioni dei browser. const/ letsono attualmente supportati negli ultimi Firefox, Safari, Edge e Chrome. Inoltre è supportato in Node e puoi usarlo ovunque sfruttando strumenti di costruzione come Babel. Puoi vedere un esempio funzionante qui: http://jsfiddle.net/ben336/rbU4t/2/

Documenti qui:

Attenzione, però, che IE9-IE11 e Edge prima del Edge 14 supportano letma sbagliano quanto sopra (non ne creano una nuova iogni volta, quindi tutte le funzioni sopra registrerebbero 3 come farebbero se lo usassimo var). Edge 14 ha finalmente capito bene.


Sfortunatamente, "let" non è ancora completamente supportato, specialmente nei dispositivi mobili. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
MattC

2
A partire da giugno '16, let è supportato in tutte le principali versioni di browser tranne iOS Safari, Opera Mini e Safari 9. I browser Evergreen lo supportano. Babel lo trasporrà correttamente per mantenere il comportamento previsto senza la modalità di alta conformità attivata.
Dan Pantry,

@DanPantry sì, è giunto il momento di un aggiornamento :) Aggiornato per riflettere meglio lo stato attuale delle cose, inclusa l'aggiunta di una menzione di const, collegamenti doc e migliori informazioni sulla compatibilità.
Ben McCormick,

Non è per questo che usiamo babel per trascrivere il nostro codice in modo che i browser che non supportano ES6 / 7 possano capire cosa sta succedendo?
pixel 67

87

Un altro modo per dirlo è che la ifunzione in è legata al momento dell'esecuzione della funzione, non al momento della creazione della funzione.

Quando si crea la chiusura, iè un riferimento alla variabile definita nell'ambito esterno, non una copia come era quando è stata creata la chiusura. Sarà valutato al momento dell'esecuzione.

La maggior parte delle altre risposte fornisce 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 dal momento che è il modo più autoesplicativo di farlo dalle risposte qui. Qualsiasi codice pubblicato funzionerà, ma opterei per una fabbrica di chiusura piuttosto che 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).


71

Quello che devi capire è che l'ambito delle variabili in JavaScript è basato sulla funzione. Questa è una differenza importante rispetto a dire c # in cui si ha l'ambito del blocco, e solo la copia della variabile in una per funzionerà.

Avvolgendolo in una funzione che valuta la restituzione della funzione come la risposta di Apphacker farà il trucco, poiché la variabile ora ha l'ambito della funzione.

Esiste anche una parola chiave let anziché var, che consentirebbe l'utilizzo della regola dell'ambito del blocco. In tal caso, definire una variabile all'interno del for farebbe il trucco. Detto questo, la parola chiave let non è una soluzione pratica a causa della compatibilità.

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


@nickf quale browser? come ho detto, ha problemi di compatibilità, con ciò intendo seri problemi di compatibilità, come non credo che let sia supportato in IE.
Eglasio,

1
@nickf sì, controlla questo riferimento: developer.mozilla.org/En/New_in_JavaScript_1.7 ... controlla la sezione delle definizioni let, c'è un esempio onclick all'interno di un ciclo
eglasius

2
@nickf hmm, in realtà devi specificare esplicitamente la versione: <script type = "application / javascript; version = 1.7" /> ... Non l'ho mai usato da nessuna parte a causa della limitazione di IE, semplicemente non lo è pratico :(
eglasio

è possibile vedere il supporto del browser per le diverse versioni qui es.wikipedia.org/wiki/Javascript
eglasius


59

Ecco un'altra variante della tecnica, simile a quella di Bjorn (apphacker), che ti consente di assegnare il valore della variabile all'interno della funzione anziché passarlo come parametro, che a volte potrebbe essere più chiaro:

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

Si noti che qualunque sia la tecnica utilizzata, la indexvariabile diventa una sorta di variabile statica, associata alla copia restituita della funzione interna. Vale a dire, le modifiche al suo valore vengono mantenute tra le chiamate. Può essere molto utile.


Grazie e la tua soluzione funziona. Ma vorrei chiedere perché questo funziona, ma lo scambio della varlinea e la returnlinea non funzionerebbe? Grazie!
Midnite

@midnite Se hai scambiato vare returnquindi la variabile non sarebbe stata assegnata prima che restituisse la funzione interna.
Boann,

53

Questo descrive l'errore comune nell'uso delle chiusure in JavaScript.

Una funzione definisce un nuovo ambiente

Prendere in considerazione:

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

Per ogni volta che makeCounterviene invocato, si {counter: 0}crea un nuovo oggetto. Inoltre, obj viene creata una nuova copia di per fare riferimento al nuovo oggetto. Pertanto, counter1e counter2sono indipendenti l'uno dall'altro.

Chiusure in anelli

L'uso di una chiusura in un ciclo è complicato.

Prendere in considerazione:

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

Si noti che counters[0]e noncounters[1] sono indipendenti. In effetti, funzionano allo stesso modo !obj

Questo perché esiste una sola copia di objcondivisa su tutte le iterazioni del ciclo, forse per motivi di prestazioni. Anche se {counter: 0}crea un nuovo oggetto in ogni iterazione, la stessa copia di objverrà semplicemente aggiornata con un riferimento all'oggetto più recente.

La soluzione è utilizzare un'altra funzione di supporto:

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

Ciò funziona perché le variabili locali nell'ambito della funzione direttamente, così come le variabili dell'argomento della funzione, vengono allocate nuove copie al momento dell'inserimento.


Piccolo chiarimento: nel primo esempio di chiusure in loop, i contatori [0] e i contatori [1] non sono indipendenti non per motivi di prestazioni. Il motivo è che var obj = {counter: 0};viene valutato prima dell'esecuzione di qualsiasi codice come indicato in: MDN var : le dichiarazioni var, ovunque si verifichino, vengono elaborate prima dell'esecuzione di qualsiasi codice.
Charidimos,

50

La soluzione più semplice sarebbe,

Invece di usare:

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

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

che avvisa "2", per 3 volte. Questo perché le funzioni anonime create in per loop, condividono la stessa chiusura e in quella chiusura il valore di iè lo stesso. Utilizzare questo per impedire la chiusura condivisa:

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

L'idea alla base di questo è, incapsulare l'intero corpo del ciclo for con un IIFE (Espressione di funzione immediatamente invocata ) e passare new_icome parametro e catturarlo come i. Poiché la funzione anonima viene eseguita immediatamente, il ivalore è diverso per ciascuna funzione definita all'interno della funzione anonima.

Questa soluzione sembra adattarsi a tale problema poiché richiederà modifiche minime al codice originale che soffre di questo problema. In effetti, questo è di progettazione, non dovrebbe essere affatto un problema!


2
Leggi qualcosa di simile in un libro una volta. Anch'io preferisco questo, dal momento che non devi toccare il tuo codice esistente (tanto) e diventa ovvio il motivo per cui l'hai fatto, una volta appreso il modello di funzione di auto-chiamata: intrappolare quella variabile nel nuovo creato scopo.
DanMan,

1
@DanMan Grazie. Le funzioni anonime di auto-chiamata sono un ottimo modo per affrontare la mancanza di portata variabile a livello di blocco di JavaScript.
Kemal Dağ,

3
Auto-chiamata o auto-invocazione non è il termine appropriato per questa tecnica, IIFE (Espressione di funzione immediatamente invocata ) è più accurata. Rif: benalman.com/news/2010/11/…
jherax,

31

prova questo più corto

  • nessun array

  • nessun extra per loop


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

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

http://jsfiddle.net/7P6EN/


1
La tua soluzione sembra produrre correttamente ma utilizza inutilmente le funzioni, perché non semplicemente console.log l'output? La domanda originale riguarda la creazione di funzioni anonime che hanno la stessa chiusura. Il problema era che, poiché hanno un'unica chiusura, il valore di i è lo stesso per ciascuno di essi. Spero tu l'abbia capito.
Kemal Dağ,

30

Ecco una semplice soluzione che utilizza forEach(funziona con 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
}

stampe:

My value: 0
My value: 1
My value: 2

27

Il problema principale con il codice mostrato dall'OP è che inon viene mai letto fino al secondo ciclo. Per dimostrare, immagina di vedere un errore all'interno del codice

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

L'errore in realtà non si verifica fino a quando non funcs[someIndex]viene eseguito (). Utilizzando questa stessa logica, dovrebbe essere evidente che anche il valore di inon viene raccolto fino a questo punto. Una volta terminato il ciclo originale, i++porta ial valore di 3cui risulta che la condizione i < 3non riesce e la fine del ciclo. A questo punto, iè 3e così quando funcs[someIndex]()viene utilizzato e iviene valutato, è 3 - ogni volta.

Per superare questo, è necessario valutare icome si incontra. Si noti che ciò è già avvenuto sotto forma di funcs[i](dove sono presenti 3 indici univoci). Esistono diversi modi per acquisire questo valore. Uno è di passarlo come parametro a una funzione che è mostrata in diversi modi già qui.

Un'altra opzione è quella di costruire un oggetto funzione che sarà in grado di chiudere sopra la variabile. Ciò può essere realizzato così

jsFiddle Demo

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

23

Le funzioni JavaScript "chiudono" l'ambito a cui hanno accesso al momento della dichiarazione e mantengono l'accesso a tale ambito anche quando le variabili in tale ambito cambiano.

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

Ogni funzione nella matrice sopra si chiude sopra l'ambito globale (globale, semplicemente perché quello è l'ambito in cui sono dichiarati).

Successivamente tali funzioni vengono invocate registrando il valore più recente di inell'ambito globale. Questa è la magia e la frustrazione della chiusura.

"Le funzioni JavaScript si chiudono sopra l'ambito in cui sono dichiarate e mantengono l'accesso a tale ambito anche quando i valori delle variabili all'interno di tale ambito cambiano."

Usando letinvece di varrisolverlo creando un nuovo ambito ogni volta che il forciclo viene eseguito, creando un ambito separato per ogni funzione da chiudere. Varie altre tecniche fanno la stessa cosa con funzioni extra.

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

( letrende le variabili con ambito di blocco. I blocchi sono indicati con parentesi graffe, ma nel caso del ciclo for la variabile di inizializzazione, inel nostro caso, è considerata dichiarata tra parentesi graffe.)


1
Ho faticato a capire questo concetto fino a quando ho letto questa risposta. Tocca un punto davvero importante: il valore di iviene impostato nell'ambito globale. Quando il forciclo termina l'esecuzione, il valore globale di iè ora 3. Pertanto, ogni volta che quella funzione viene invocata nell'array (usando, diciamo funcs[j]), iin quella funzione fa riferimento alla ivariabile globale (che è 3).
Modermo,

13

Dopo aver letto varie soluzioni, vorrei aggiungere che il motivo per cui tali soluzioni funzionano è fare affidamento sul concetto di catena di scopo . È il modo in cui JavaScript risolve una variabile durante l'esecuzione.

  • Ogni definizione di funzione costituisce un ambito costituito da tutte le variabili locali dichiarate da vare la sua arguments.
  • Se abbiamo una funzione interna definita all'interno di un'altra funzione (esterna), questa forma una catena e verrà utilizzata durante l'esecuzione
  • Quando viene eseguita una funzione, il runtime valuta le variabili cercando nella catena dell'ambito . Se una variabile può essere trovata in un determinato punto della catena, smetterà di cercare e usarla, altrimenti continuerà fino a raggiungere l'ambito globale a cui appartiene window.

Nel codice iniziale:

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 funcsviene eseguito, la catena di portata sarà function inner -> global. Poiché la variabile inon può essere trovata in function inner(né dichiarata usando varné passata come argomento), continua a cercare, fino a quando il valore di non iviene infine trovato nell'ambito globale che è window.i.

Avvolgendolo in una funzione esterna o definire esplicitamente una funzione di supporto come ha fatto harto o utilizzare una funzione anonima come Bjorn ha fatto:

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 funcsviene eseguito, ora sarà la catena di portata function inner -> function outer. Questa volta ipuò essere trovata nell'ambito della funzione esterna che viene eseguita 3 volte nel ciclo for, ogni volta che il valore è iassociato correttamente. Non utilizzerà il valore di window.iquando interno eseguito.

Maggiori dettagli possono essere trovati qui
Include l'errore comune nella creazione della chiusura nel loop come quello che abbiamo qui, oltre al motivo per cui abbiamo bisogno di chiusura e considerazione delle prestazioni.


Raramente scriviamo questo esempio di codice in tempo reale, ma penso che serva un buon esempio per capire il fondamentale. Una volta che abbiamo in mente lo scopo e il modo in cui si sono incatenati insieme, è più chiaro capire perché altri modi 'moderni' come il Array.prototype.forEach(function callback(el) {})naturale funzionano: il callback che viene passato forma naturalmente l'ambito di wrapping con el correttamente associato in ogni iterazione di forEach. Quindi ogni funzione interna definita nel callback sarà in grado di usare il giusto elvalore
wpding

13

Con le nuove funzionalità di scoping a livello di blocco ES6 viene gestito:

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
}

Il codice nella domanda dell'OP viene sostituito letinvece di var.


constfornisce lo stesso risultato e dovrebbe essere usato quando il valore di una variabile non cambierà. Tuttavia, l'uso di constall'interno dell'inizializzatore del ciclo for è implementato in modo errato in Firefox e deve ancora essere risolto. Invece di essere dichiarato all'interno del blocco, viene dichiarato all'esterno del blocco, il che si traduce in una nuova dichiarazione della variabile, che a sua volta provoca un errore. L'uso di letall'interno dell'inizializzatore è implementato correttamente in Firefox, quindi non c'è bisogno di preoccuparsi lì.

10

Sono sorpreso che nessuno abbia ancora suggerito di utilizzare la forEachfunzione per evitare (ri) utilizzare le variabili locali. In effetti, non sto più usando for(var i ...)per questo motivo.

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

// modificato per usare al forEachposto della mappa.


3
.forEach()è un'opzione molto migliore se in realtà non stai mappando nulla, e Daryl ha suggerito che 7 mesi prima di pubblicare, quindi non c'è nulla di cui stupirsi.
JLRishe

Questa domanda non riguarda il loop su un array
jherax,

Bene, vuole creare una serie di funzioni, questo esempio mostra come farlo senza coinvolgere una variabile globale.
Christian Landgren,

9

Questa domanda mostra davvero la storia di JavaScript! Ora possiamo evitare l'ambito del blocco con le funzioni freccia e gestire i loop direttamente dai nodi DOM utilizzando i metodi 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>


8

Il motivo per cui il tuo esempio originale non ha funzionato è che tutte le chiusure che hai creato nel loop facevano riferimento allo stesso frame. In effetti, avere 3 metodi su un oggetto con una sola ivariabile. Tutti hanno stampato lo stesso valore.


8

Prima di tutto, capire cosa c'è che non va in questo codice:

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
}

Qui quando l' funcs[]array viene inizializzato, iviene incrementato, l' funcsarray viene inizializzato e la dimensione funcdell'array diventa 3, quindi i = 3,. Ora quando funcs[j]()viene chiamato, viene nuovamente utilizzato la variabile i, che è già stata incrementata a 3.

Ora per risolvere questo, abbiamo molte opzioni. Di seguito sono due:

  1. Possiamo inizializzare icon leto inizializzare una nuova variabile indexcon lete renderla uguale a i. Pertanto, quando viene effettuata la chiamata, indexverrà utilizzato e il relativo ambito terminerà dopo l'inizializzazione. E per chiamare, indexverrà inizializzato di nuovo:

    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. Un'altra opzione può essere quella di introdurre una tempFuncche restituisce la funzione effettiva:

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

8

Usa la struttura di chiusura , questo ridurrebbe il tuo extra per il loop. Puoi farlo in un unico ciclo for:

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

7

Verificheremo cosa succede effettivamente quando dichiari vare let uno per uno.

Caso 1 : utilizzovar

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

Ora apri la finestra della tua console Chrome premendo F12 e aggiorna la pagina. Spendi ogni 3 funzioni all'interno dell'array. Vedrai una proprietà chiamata. [[Scopes]]Espandi quella. Vedrai un oggetto array chiamato "Global", espanderlo. Troverete una proprietà 'i'dichiarata nell'oggetto con valore 3.

inserisci qui la descrizione dell'immagine

inserisci qui la descrizione dell'immagine

Conclusione:

  1. Quando dichiari una variabile usando 'var'al di fuori di una funzione, diventa una variabile globale (puoi controllare digitando io window.inella finestra della console. Restituirà 3).
  2. La funzione annominous che hai dichiarato non chiamerà e controllerà il valore all'interno della funzione a meno che non invochi le funzioni.
  3. Quando si richiama la funzione, console.log("My value: " + i)prende il valore dal suo Globaloggetto e visualizza il risultato.

CASE2: usando let

Ora sostituisci 'var'con'let'

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

Fai la stessa cosa, vai agli ambiti. Ora vedrai due oggetti "Block"e "Global". Ora espandi Blockoggetto, vedrai che 'i' è definito lì, e la cosa strana è che, per ogni funzione, il valore iè diverso (0, 1, 2).

inserisci qui la descrizione dell'immagine

Conclusione:

Quando dichiari una variabile usando 'let'anche al di fuori della funzione ma all'interno del ciclo, questa variabile non sarà una variabile Globale, diventerà una Blockvariabile di livello che è disponibile solo per la stessa funzione. Per questo motivo stiamo ottenendo un valore idiverso per ogni funzione quando invochiamo le funzioni.

Per maggiori dettagli su come funziona più da vicino, segui il fantastico tutorial video https://youtu.be/71AtaJpJHw0


4

È possibile utilizzare un modulo dichiarativo per elenchi di dati come query-js (*). In queste situazioni trovo personalmente un approccio dichiarativo meno sorprendente

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

È quindi possibile utilizzare il secondo ciclo e ottenere il risultato previsto oppure è possibile farlo

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

(*) Sono l'autore di query-js e quindi distorto nell'usarlo, quindi non prendere le mie parole come una raccomandazione per detta libreria solo per l'approccio dichiarativo :)


1
Mi piacerebbe una spiegazione del voto negativo. Il codice risolve il problema a portata di mano. Sarebbe utile sapere come migliorare potenzialmente il codice
Rune FS,

1
Che cosa è Query.range(0,3)? Questo non fa parte dei tag per questa domanda. Inoltre, se si utilizza una libreria di terze parti, è possibile fornire il collegamento della documentazione.
jherax,

1
@jherax sono o ovviamente evidenti miglioramenti. Grazie per il commento. Avrei potuto giurare che c'era già un collegamento. Detto questo, il post era abbastanza inutile, immagino :). La mia idea iniziale di tenerlo fuori era perché non stavo cercando di spingere l'uso della mia biblioteca ma più l'idea dichiarativa. Tuttavia, con il senno di poi, concordo pienamente che il link dovrebbe essere lì
Rune FS

4

Preferisco usare la forEachfunzione, che ha una sua chiusura con la creazione di uno pseudo intervallo:

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
}

Sembra più brutto delle gamme in altre lingue, ma IMHO è meno mostruoso di altre soluzioni.


Preferirlo a cosa? Questo sembra essere un commento in risposta ad un'altra risposta. Non affronta affatto la vera domanda (poiché non stai assegnando una funzione, da chiamare in seguito, ovunque).
Quentin,

È collegato esattamente al problema menzionato: come eseguire l'iterazione sicura senza problemi di chiusura
Rax Wunter,

Ora non sembra significativamente diverso dalla risposta accettata.
Quentin,

No. Nella risposta accettata si suggerisce di usare "un certo array", ma abbiamo a che fare con un intervallo nella risposta, sono cose assolutamente diverse, che purtroppo non hanno una buona soluzione in js, quindi la mia risposta sta cercando di risolvere il problema in modo positivo e pratico
Rax Wunter il

@Quentin Vorrei raccomandare di indagare sulla soluzione prima di prendere in considerazione
Rax Wunter il

4

E ancora un'altra soluzione: invece di creare un altro loop, basta associare la thisfunzione 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
}

Legando questo , risolve anche il problema.


3

Molte soluzioni sembrano corrette ma non menzionano il suo nome, Curryingche è un modello di progettazione di programmazione funzionale per situazioni come qui. 3-10 volte più veloce del bind a seconda del browser.

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

Guarda il miglioramento delle prestazioni in diversi browser .


@TinyGiant L'esempio con la funzione restituita continua a essere ottimizzato per le prestazioni. Non salterei sulle funzioni delle frecce del carrozzone come tutti i blogger JavaScript. Sembrano belli e puliti, ma promuovono le funzioni di scrittura in linea invece di utilizzare funzioni predefinite. Questa può essere una trappola non ovvia in luoghi caldi. Un altro problema è che non sono solo zucchero sintattico perché stanno eseguendo attacchi non necessari creando così chiusure avvolgenti.
Pawel,

2
Avvertimento per i futuri lettori: questa risposta applica in modo impreciso il termine Currying . "Il curriculum è quando si scompone una funzione che accetta più argomenti in una serie di funzioni che prendono parte degli argomenti." . Questo codice non fa nulla del genere. Tutto quello che hai fatto qui è prendere il codice dalla risposta accettata, spostare alcune cose, cambiare lo stile e nominare un po ', quindi chiamarlo curry, che non è categoricamente.

3

Il tuo codice non funziona, perché quello che fa è:

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?

Ora la domanda è: qual è il valore della variabile iquando viene chiamata la funzione? Poiché il primo ciclo viene creato con la condizione di i < 3, si interrompe immediatamente quando la condizione è falsa, quindi lo è i = 3.

Devi capire che, nel momento in cui vengono create le tue funzioni, nessuno dei loro codici viene eseguito, viene salvato solo per dopo. E così quando vengono chiamati in seguito, l'interprete li esegue e chiede: "Qual è il valore attuale di i?"

Quindi, il tuo obiettivo è innanzitutto salvare il valore di ifunzione e solo dopo salvare la funzione funcs. Questo potrebbe essere fatto ad esempio in questo modo:

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
}

In questo modo, ogni funzione avrà la propria variabile xe la impostiamo xsul valore di iin ogni iterazione.

Questo è solo uno dei molteplici modi per risolvere questo problema.


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
}

3

Utilizzare let (ambito bloccato) anziché 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]();                      
}

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.