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]();
}
Produce esto:
Mi valor: 3
Mi valor: 3
Mi valor: 3
Mientras que me gustaría que salga:
Mi valor: 0
Mi valor: 1
Mi valor: 2
El mismo problema se produce cuando el retraso en la ejecución de la función se debe al uso de escuchas de eventos:
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 código asincrónico, por ejemplo, usando Promesas:
// 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));
}
Editar: También es evidente en for in
y for of
bucles:
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();
}
¿Cuál es la solución a este problema básico?
javascript
loops
closures
nickf
fuente
fuente
funcs
ser una matriz, si estás usando índices numéricos? Solo un aviso.Respuestas:
Bueno, el problema es que la variable
i
, dentro de cada una de sus funciones anónimas, está vinculada a la misma variable fuera de la función.Solución clásica: cierres
Lo que desea hacer es vincular la variable dentro de cada función a un valor separado e inmutable fuera de la función:
Dado que no hay un alcance de bloque en JavaScript, solo el alcance de la función, al envolver la creación de la función en una nueva función, se asegura de que el valor de "i" permanezca como lo deseaba.
Solución ES5.1: para cada uno
Con la disponibilidad relativamente generalizada de la
Array.prototype.forEach
función (en 2015), vale la pena señalar que en aquellas situaciones que involucran la iteración principalmente sobre una matriz de valores,.forEach()
proporciona una forma limpia y natural de obtener un cierre distinto para cada iteración. Es decir, suponiendo que tiene algún tipo de matriz que contiene valores (referencias DOM, objetos, lo que sea), y surge el problema de configurar devoluciones de llamada específicas para cada elemento, puede hacer esto:La idea es que cada invocación de la función de devolución de llamada utilizada con el
.forEach
bucle será su propio cierre. El parámetro pasado a ese controlador es el elemento de matriz específico para ese paso particular de la iteración. Si se usa en una devolución de llamada asincrónica, no colisionará con ninguna de las otras devoluciones de llamada establecidas en otros pasos de la iteración.Si está trabajando en jQuery, la
$.each()
función le brinda una capacidad similar.Solución ES6:
let
ECMAScript 6 (ES6) presenta palabras clave nuevas
let
y conconst
un alcance diferente alvar
de las variables basadas. Por ejemplo, en un bucle con unlet
índice basado en, cada iteración a través del bucle tendrá una nueva variablei
con alcance de bucle, por lo que su código funcionará como espera. Hay muchos recursos, pero recomendaría la publicación de alcance de bloque de 2ality como una gran fuente de información.Sin embargo, tenga en cuenta que IE9-IE11 y Edge antes de Edge 14 admiten
let
pero se equivocan en lo anterior (no crean una nuevai
cada vez, por lo que todas las funciones anteriores registrarían 3 como lo harían si las usáramosvar
). Edge 14 finalmente lo hace bien.fuente
function createfunc(i) { return function() { console.log("My value: " + i); }; }
Todavía no se cierra porque usa la variablei
?Function.bind()
definitivamente es preferible ahora, consulte stackoverflow.com/a/19323214/785541 ..bind()
es "la respuesta correcta" no es correcta. Cada uno tiene su propio lugar. Con.bind()
usted no puede vincular argumentos sin vincular elthis
valor. También obtienes una copia deli
argumento sin la capacidad de mutarlo entre llamadas, lo que a veces es necesario. Por lo tanto, son construcciones bastante diferentes, sin mencionar que las.bind()
implementaciones han sido históricamente lentas. Claro, en el ejemplo simple, cualquiera funcionaría, pero los cierres son un concepto importante para entender, y de eso se trataba la pregunta.Tratar:
Editar (2014):
Personalmente, creo que la respuesta más reciente de
.bind
@ Aust sobre el uso es la mejor manera de hacer este tipo de cosas ahora. Hay también lo-dash / subrayado es_.partial
cuando no se necesita o quiere meterse conbind
'sthisArg
.fuente
}(i));
?i
como argumentoindex
de la función.Otra forma que aún no se ha mencionado es el uso de
Function.prototype.bind
ACTUALIZAR
Como lo señalaron @squint y @mekdev, obtienes un mejor rendimiento creando primero la función fuera del ciclo y luego vinculando los resultados dentro del ciclo.
fuente
_.partial
.bind()
será obsoleto en gran medida con las características de ECMAScript 6. Además, esto realmente crea dos funciones por iteración. Primero el anónimo, luego el generado por.bind()
. Mejor uso sería crearlo fuera del ciclo, luego.bind()
dentro.bind
se usa. He agregado otro ejemplo por sus sugerencias.this
.Usando una expresión de función invocada inmediatamente , la forma más simple y fácil de encerrar una variable de índice:
Esto envía el iterador
i
a la función anónima que definimos comoindex
. Esto crea un cierre, donde la variablei
se guarda para su uso posterior en cualquier funcionalidad asincrónica dentro del IIFE.fuente
i
es qué, cambiaría el nombre del parámetro de funciónindex
.index
lugar dei
.var 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](); }
.forEach()
, pero una gran parte del tiempo, cuando uno está empezando con una matriz,forEach()
es una opción buena, como:var nums [4, 6, 7]; var funcs = {}; nums.forEach(function (num, i) { funcs[i] = function () { console.log(num); }; });
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?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 asignadoilocal
. ¿Pero no debería cada iteración tener la suya propiailocal
?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 :Reiterado por énfasis:
Podemos ver esto comprobando
ilocal
antes de declararlo en cada iteración: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 utilizailocal
(más explicación a continuación):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:Actualizar
Con ES6 ahora convencional, ahora podemos usar la nueva
let
palabra clave para crear variables de ámbito de bloque:¡Mira lo fácil que es ahora! Para obtener más información, consulte esta respuesta , en la que se basa mi información.
fuente
let
yconst
. Si esta respuesta se ampliara para incluir eso, sería mucho más útil a nivel mundial en mi opinión.let
yi=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++ }
:? (podría reemplazar window.open () con getelementbyid ......)i
sus funciones de tiempo de espera, por lo que no necesita un cierreCon ES6 ahora ampliamente admitido, la mejor respuesta a esta pregunta ha cambiado. ES6 proporciona las palabras clave
let
yconst
para esta circunstancia exacta. En lugar de perder el tiempo con los cierres, solo podemos usarlet
para establecer una variable de alcance de bucle como esta:val
luego apuntará a un objeto que sea específico para ese giro particular del bucle y devolverá el valor correcto sin la notación de cierre adicional. Obviamente, esto simplifica significativamente este problema.const
es similar alet
la restricción adicional de que el nombre de la variable no puede ser devuelto a una nueva referencia después de la asignación inicial.El soporte del navegador ahora está aquí para aquellos que se dirigen a las últimas versiones de los navegadores.
const
/let
actualmente son compatibles con los últimos Firefox, Safari, Edge y Chrome. También es compatible con Node, y puede usarlo en cualquier lugar aprovechando herramientas de compilación como Babel. Puede ver un ejemplo de trabajo aquí: http://jsfiddle.net/ben336/rbU4t/2/Documentos aquí:
Sin embargo, tenga en cuenta que IE9-IE11 y Edge antes de Edge 14 admiten
let
pero se equivocan en lo anterior (no crean una nuevai
cada vez, por lo que todas las funciones anteriores registrarían 3 como lo harían si las usáramosvar
). Edge 14 finalmente lo hace bien.fuente
Otra forma de decirlo es que el
i
en su función está vinculado al momento de ejecutar la función, no al momento de crear la función.Cuando creas el cierre,
i
es una referencia a la variable definida en el ámbito externo, no una copia de la misma como era cuando creó el cierre. Será evaluado en el momento de la ejecución.La mayoría de las otras respuestas proporcionan formas de evitarlo creando otra variable que no cambiará el valor por usted.
Solo pensé en agregar una explicación para mayor claridad. Para una solución, personalmente, iría con la de Harto, ya que es la forma más autoexplicativa de hacerlo a partir de las respuestas aquí. Cualquiera de los códigos publicados funcionará, pero optaría por una fábrica de cierre en lugar de tener que escribir un montón de comentarios para explicar por qué estoy declarando una nueva variable (Freddy y 1800) o tengo una sintaxis de cierre incrustada extraña (apphacker).
fuente
Lo que debe comprender es que el alcance de las variables en JavaScript se basa en la función. Esta es una diferencia importante que decir c # donde tiene un alcance de bloque, y simplemente copiando la variable a una dentro de for funcionará.
Ajustarlo en una función que evalúa devolver la función como la respuesta de apphacker hará el truco, ya que la variable ahora tiene el alcance de la función.
También hay una palabra clave let en lugar de var, que permitiría usar la regla de alcance de bloque. En ese caso, definir una variable dentro de for haría el truco. Dicho esto, la palabra clave let no es una solución práctica debido a la compatibilidad.
fuente
Aquí hay otra variación de la técnica, similar a la de Bjorn (apphacker), que le permite asignar el valor de la variable dentro de la función en lugar de pasarlo como un parámetro, que a veces puede ser más claro:
Tenga en cuenta que, sea cual sea la técnica que utilice, la
index
variable se convierte en una especie de variable estática, vinculada a la copia devuelta de la función interna. Es decir, los cambios en su valor se conservan entre llamadas. Puede ser muy útil.fuente
var
línea y lareturn
línea no funcionaría. ¡Gracias!var
yreturn
luego la variable no se asignaría antes de devolver la función interna.Esto describe el error común al usar cierres en JavaScript.
Una función define un nuevo entorno.
Considerar:
Por cada vez que
makeCounter
se invoca,{counter: 0}
se crea un nuevo objeto. Además,obj
se crea una nueva copia de para hacer referencia al nuevo objeto. Por lo tanto,counter1
ycounter2
son independientes entre sí.Cierres en bucles
Usar un cierre en un bucle es complicado.
Considerar:
Tenga en cuenta que
counters[0]
ycounters[1]
son no independientes. De hecho, ¡operan de la misma maneraobj
!Esto se debe a que solo hay una copia de
obj
compartida en todas las iteraciones del bucle, tal vez por razones de rendimiento. Aunque{counter: 0}
crea un nuevo objeto en cada iteración, la misma copiaobj
se actualizará con una referencia al objeto más nuevo.La solución es usar otra función auxiliar:
Esto funciona porque las variables locales en el ámbito de la función directamente, así como las variables de argumento de la función, se asignan nuevas copias al ingresar.
fuente
var obj = {counter: 0};
se evalúa antes de que se ejecute cualquier código como se indica en: MDN var : las declaraciones de var, donde sea que ocurran, se procesan antes de que se ejecute cualquier código.La solución más simple sería,
En lugar de usar:
que alerta "2", por 3 veces. Esto se debe a que las funciones anónimas creadas en for loop, comparten el mismo cierre, y en ese cierre, el valor de
i
es el mismo. Use esto para evitar el cierre compartido:La idea detrás de esto es encapsular todo el cuerpo del bucle for con una IIFE (Expresión de función invocada inmediatamente) y pasarla
new_i
como un parámetro y capturarlo comoi
. Como la función anónima se ejecuta inmediatamente, eli
valor es diferente para cada función definida dentro de la función anónima.Esta solución parece ajustarse a cualquier problema, ya que requerirá cambios mínimos en el código original que sufre este problema. De hecho, esto es por diseño, ¡no debería ser un problema en absoluto!
fuente
prueba este más corto
sin matriz
sin extra para loop
http://jsfiddle.net/7P6EN/
fuente
Aquí hay una solución simple que usa
forEach
(vuelve a IE9):Huellas dactilares:
fuente
El principal problema con el código que muestra el OP es que
i
nunca se lee hasta el segundo bucle. Para demostrarlo, imagine ver un error dentro del códigoEl error en realidad no ocurre hasta que
funcs[someIndex]
se ejecuta()
. Usando esta misma lógica, debería ser evidente que el valor dei
tampoco se recopila hasta este punto tampoco. Una vez que finaliza el ciclo original,i++
llevai
al valor de lo3
cual resulta en lai < 3
falla de la condición y el final del ciclo. En este punto,i
es3
y cuandofuncs[someIndex]()
se usa, yi
se evalúa, es 3, cada vez.Para superar esto, debe evaluar
i
cómo se encuentra. Tenga en cuenta que esto ya ha sucedido en forma defuncs[i]
(donde hay 3 índices únicos). Hay varias formas de capturar este valor. Una es pasarlo como parámetro a una función que ya se muestra de varias maneras aquí.Otra opción es construir un objeto de función que pueda cerrarse sobre la variable. Eso se puede lograr así
jsFiddle Demo
fuente
Las funciones de JavaScript "cierran" el alcance al que tienen acceso tras la declaración, y retienen el acceso a ese alcance incluso cuando cambian las variables en ese alcance.
Cada función en la matriz anterior se cierra sobre el alcance global (global, simplemente porque ese es el alcance en el que se declaran).
Más tarde, esas funciones se invocan registrando el valor más actual de
i
en el ámbito global. Esa es la magia y la frustración del cierre."Las funciones de JavaScript se cierran sobre el alcance en el que se declaran y retienen el acceso a ese alcance incluso cuando cambian los valores variables dentro de ese alcance".
El uso en
let
lugar devar
resuelve esto creando un nuevo alcance cada vez que sefor
ejecuta el bucle, creando un alcance separado para cada función para cerrar. Varias otras técnicas hacen lo mismo con funciones adicionales.(
let
hace que las variables tengan un alcance de bloque. Los bloques se indican mediante llaves, pero en el caso del bucle for, la variable de inicialización,i
en nuestro caso, se considera declarada en las llaves).fuente
i
se establece en el ámbito global. Cuando elfor
ciclo termina de ejecutarse, el valor global dei
ahora es 3. Por lo tanto, cada vez que se invoca esa función en la matriz (usando, por ejemplofuncs[j]
), lai
función in hace referencia a lai
variable global (que es 3).Después de leer varias soluciones, me gustaría agregar que la razón por la que esas soluciones funcionan es confiar en el concepto de cadena de alcance . Es la forma en que JavaScript resuelve una variable durante la ejecución.
var
y susarguments
.window
.En el código inicial:
Cuando
funcs
se ejecuta, la cadena de alcance seráfunction inner -> global
. Como la variablei
no se puede encontrar enfunction inner
(ni declarada usandovar
ni pasada como argumentos), continúa buscando, hasta que el valor dei
finalmente se encuentra en el ámbito global que eswindow.i
.Al envolverlo en una función externa, defina explícitamente una función auxiliar como lo hizo harto o use una función anónima como lo hizo Bjorn :
Cuando
funcs
se ejecuta, ahora la cadena de alcance seráfunction inner -> function outer
. Este tiempoi
se puede encontrar en el alcance de la función externa que se ejecuta 3 veces en el ciclo for, cada vez tiene un valori
enlazado correctamente. No usará el valor dewindow.i
cuando se ejecute internamente.Se pueden encontrar más detalles aquí.
Incluye el error común al crear un cierre en el ciclo como lo que tenemos aquí, así como por qué necesitamos el cierre y la consideración del rendimiento.
fuente
Array.prototype.forEach(function callback(el) {})
funcionan de manera natural otras formas `` modernas '' : la devolución de llamada que se pasa naturalmente forma el alcance envolvente con el correctamente enlazado en cada iteración deforEach
. Por lo tanto, cada función interna definida en la devolución de llamada podrá usar elel
valor correctoCon las nuevas características de ES6 se gestiona el alcance del nivel de bloque:
El código en la pregunta de OP se reemplaza con en
let
lugar devar
.fuente
const
proporciona el mismo resultado y debe usarse cuando el valor de una variable no cambiará. Sin embargo, el uso deconst
dentro del inicializador del bucle for se implementa incorrectamente en Firefox y aún no se ha corregido. En lugar de ser declarado dentro del bloque, se declara fuera del bloque, lo que resulta en una redeclaración de la variable, lo que a su vez resulta en un error. El uso dellet
interior del inicializador se implementa correctamente en Firefox, por lo que no debe preocuparse allí.Me sorprende que nadie haya sugerido usar la
forEach
función para evitar (re) usar mejor las variables locales. De hecho, ya no estoy usandofor(var i ...)
nada por esta razón.// editado para usar en
forEach
lugar de mapa.fuente
.forEach()
es una opción mucho mejor si en realidad no está mapeando nada, y Daryl sugirió que 7 meses antes de publicar, por lo que no hay nada de qué sorprenderse.¡Esta pregunta realmente muestra la historia de JavaScript! Ahora podemos evitar el alcance del bloque con funciones de flecha y manejar bucles directamente desde nodos DOM utilizando métodos Object.
fuente
La razón por la que su ejemplo original no funcionó es porque todos los cierres que creó en el bucle hacían referencia al mismo marco. En efecto, tener 3 métodos en un objeto con una sola
i
variable. Todos imprimieron el mismo valor.fuente
En primer lugar, entienda lo que está mal con este código:
Aquí, cuando la
funcs[]
matriz se inicializa,i
se incrementa, lafuncs
matriz se inicializa y el tamaño de lafunc
matriz se convierte en 3, entoncesi = 3,
. Ahora cuandofuncs[j]()
se llama al, nuevamente está usando la variablei
, que ya se ha incrementado a 3.Ahora para resolver esto, tenemos muchas opciones. A continuación hay dos de ellos:
Podemos inicializar
i
conlet
o inicializar una nueva variableindex
conlet
y que tenga la mismai
. Entonces, cuando se realiza la llamada,index
se utilizará y su alcance finalizará después de la inicialización. Y para llamar,index
se inicializará nuevamente:Otra opción puede ser introducir una
tempFunc
que devuelva la función real:fuente
Utilice la estructura de cierre , esto reduciría su bucle adicional. Puedes hacerlo en un solo ciclo for:
fuente
Caso1 : usando
var
Ahora abra la ventana de la consola de Chrome presionando F12 y actualice la página. Gasta cada 3 funciones dentro de la matriz. Verás una propiedad llamada.
[[Scopes]]
Expande esa. Verá un objeto de matriz llamado"Global"
, expanda ese. Encontrará una propiedad'i'
declarada en el objeto que tiene valor 3.Conclusión:
'var'
fuera de una función, se convierte en una variable global (puede verificar escribiendoi
owindow.i
en la ventana de la consola, devolverá 3).console.log("My value: " + i)
toma el valor de suGlobal
objeto y muestra el resultado.CASO2: usando let
Ahora reemplace el
'var'
con'let'
Haz lo mismo, ve a los ámbitos. Ahora verá dos objetos
"Block"
y"Global"
. Ahora expanda elBlock
objeto, verá que 'i' está definido allí, y lo extraño es que, para cada función, el valor ifi
es diferente (0, 1, 2).Conclusión:
Cuando declara una variable usando
'let'
incluso fuera de la función pero dentro del ciclo, esta variable no será una variable Global, se convertirá en unaBlock
variable de nivel que solo está disponible para la misma función. Esa es la razón, estamos obteniendo el valor dei
diferentes para cada función cuando invocamos las funciones.Para obtener más detalles sobre cómo funciona más de cerca, consulte el increíble video tutorial https://youtu.be/71AtaJpJHw0
fuente
Puede usar un módulo declarativo para listas de datos como query-js (*). En estas situaciones personalmente encuentro menos sorprendente un enfoque declarativo
Luego, podría usar su segundo ciclo y obtener el resultado esperado o podría hacer
(*) Soy el autor de query-js y por lo tanto estoy predispuesto a usarlo, así que no tome mis palabras como una recomendación para dicha biblioteca solo para el enfoque declarativo :)
fuente
Query.range(0,3)
? Esto no es parte de las etiquetas para esta pregunta. Además, si utiliza una biblioteca de terceros, puede proporcionar el enlace de la documentación.Prefiero usar la
forEach
función, que tiene su propio cierre con la creación de un pseudo rango:Eso parece más feo que los rangos en otros idiomas, pero en mi humilde opinión, menos monstruoso que otras soluciones.
fuente
Y otra solución más: en lugar de crear otro bucle, simplemente vincula la
this
función de retorno.Al vincular esto , también se resuelve el problema.
fuente
Muchas soluciones parecen correctas, pero no mencionan que se llama,
Currying
que es un patrón de diseño de programación funcional para situaciones como esta. De 3 a 10 veces más rápido que el enlace, dependiendo del navegador.Vea la ganancia de rendimiento en diferentes navegadores .
fuente
Su código no funciona, porque lo que hace es:
Ahora la pregunta es, ¿cuál es el valor de la variable
i
cuando se llama a la función? Debido a que el primer bucle se crea con la condición dei < 3
, se detiene inmediatamente cuando la condición es falsa, por lo que es asíi = 3
.Debe comprender que, cuando se crean sus funciones, ninguno de sus códigos se ejecuta, solo se guarda para más adelante. Entonces, cuando se les llama más tarde, el intérprete los ejecuta y pregunta: "¿Cuál es el valor actual de
i
?"Por lo tanto, su objetivo es guardar primero el valor de
i
funcionar y solo después guardar la función enfuncs
. Esto podría hacerse, por ejemplo, de esta manera:De esta forma, cada función tendrá su propia variable
x
y estableceremos estox
en el valor dei
en cada iteración.Esta es solo una de las múltiples formas de resolver este problema.
fuente
fuente
Use let (bloqueado-alcance) en lugar de var.
fuente