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 i
variabile. 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 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 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.
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 ilocal
nel 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 ilocal
variabili 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 let
parola 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.
funcs
essere un array, se stai utilizzando indici numerici? Solo un avviso.