Llegué un poco tarde a la fiesta, pero hoy estaba explorando este problema y noté que muchas de las respuestas no abordan completamente cómo Javascript trata los ámbitos, que es esencialmente a lo que se reduce todo esto.
Entonces, como muchos otros mencionaron, el problema es que la función interna hace referencia a la misma i
variable. Entonces, ¿por qué no creamos una nueva variable local en cada iteración, y tenemos la función interna de referencia?
//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]();
}
Al igual que antes, donde cada función interna generaba el último valor asignado i
, ahora cada función interna solo genera el último valor asignado ilocal
. ¿Pero no debería cada iteración tener la suya propia ilocal
?
Resulta que ese es el problema. Cada iteración comparte el mismo alcance, por lo que cada iteración después de la primera solo se sobrescribe ilocal
. De MDN :
Importante: JavaScript no tiene alcance de bloque. Las variables introducidas con un bloque tienen un alcance para la función o script que contiene, y los efectos de establecerlas persisten más allá del bloque mismo. En otras palabras, las declaraciones de bloque no introducen un alcance. Aunque los bloques "independientes" son una sintaxis válida, no desea utilizar bloques independientes en JavaScript, porque no hacen lo que usted cree que hacen, si cree que hacen algo como esos bloques en C o Java.
Reiterado por énfasis:
JavaScript no tiene alcance de bloque. Las variables introducidas con un bloque tienen un alcance a la función o script que lo contiene
Podemos ver esto comprobando ilocal
antes de declararlo en cada iteración:
//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;
}
Esto es exactamente por qué este error es tan complicado. Aunque esté redeclarando una variable, Javascript no arrojará un error y JSLint ni siquiera lanzará una advertencia. Esta es también la razón por la cual la mejor manera de resolver esto es aprovechar los cierres, que es esencialmente la idea de que en Javascript, las funciones internas tienen acceso a variables externas porque los ámbitos internos "encierran" ámbitos externos.
Esto también significa que las funciones internas "mantienen" las variables externas y las mantienen vivas, incluso si la función externa regresa. Para utilizar esto, creamos y llamamos a una función de contenedor únicamente para crear un nuevo ámbito, declarar ilocal
en el nuevo ámbito y devolver una función interna que utiliza ilocal
(más explicación a continuación):
//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 creación de la función interna dentro de una función contenedora le da a la función interna un entorno privado al que solo puede acceder, un "cierre". Por lo tanto, cada vez que llamamos a la función de contenedor, creamos una nueva función interna con su propio entorno separado, asegurando que las ilocal
variables no colisionen y se sobrescriban entre sí. Algunas optimizaciones menores dan la respuesta final que muchos otros usuarios de SO dieron:
//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);
};
}
Actualizar
Con ES6 ahora convencional, ahora podemos usar la nueva let
palabra clave para crear variables de ámbito de bloque:
//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]();
}
¡Mira lo fácil que es ahora! Para obtener más información, consulte esta respuesta , en la que se basa mi información.
funcs
ser una matriz, si estás usando índices numéricos? Solo un aviso.