javascript - programacion - que es closure en español
Cierre de JavaScript dentro de bucles-ejemplo práctico simple (30)
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
}
Produce esto:
Mi valor: 3
Mi valor: 3
Mi valor: 3
Mientras que me gustaría que la salida:
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 detectores de eventos:
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 código asíncrono, por ejemplo, utilizando promesas:
// 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.
}
¿Cuál es la solución a este problema básico?
Vamos a comprobar, lo que realmente sucede cuando usted declara
var
ylet
uno por uno.
Caso 1 : utilizandovar
<script>
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs[i] = function () {
debugger;
console.log("My value: " + i);
};
}
console.log(funcs);
</script>
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:
- Cuando declara una variable
''var''
fuera de una función, se convierte en variable global (puede verificar escribiendoi
owindow.i
en la ventana de la consola. Volverá 3). - La función anominal que declaró no llamará ni verificará el valor dentro de la función a menos que invoque las funciones.
- Cuando invocas la función,
console.log("My value: " + i)
toma el valor de suGlobal
objeto y muestra el resultado.
CASE2: usando let
Ahora reemplaza el ''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>
Haz lo mismo, ve a los ámbitos. Ahora verás dos objetos "Block"
y "Global"
. Ahora expandir Block
objeto, verá que ''i'' está definido allí, y lo extraño es que, para cada función, el valor i
es diferente (0, 1, 2).
Conclusión:
Cuando declara una variable usando ''let''
incluso fuera de la función pero dentro del bucle, esta variable no será una variable global, se convertirá en una Block
variable de nivel que solo está disponible para la misma función. Esa es la razón, estamos obteniendo valores de i
diferentes para cada función cuando invocamos las funciones.
Para obtener más detalles acerca de cómo funciona más cerca, consulte el impresionante tutorial en video https://youtu.be/71AtaJpJHw0
prueba este mas corto
sin matriz
no extra para bucle
for (var i = 0; i < 3; i++) {
createfunc(i)();
}
function createfunc(i) {
return function(){console.log("My value: " + i);};
}
Aquí hay otra variación en 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:
for (var i = 0; i < 3; i++) {
funcs[i] = (function() {
var index = i;
return function() {
console.log("My value: " + index);
}
})();
}
Tenga en cuenta que cualquiera que sea la técnica que utilice, la variable de index
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 las llamadas. Puede ser muy útil.
Aquí hay una solución simple que usa forEach
(funciona de nuevo a 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
}
Huellas dactilares:
My value: 0 My value: 1 My value: 2
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 independiente e inalterable fuera de la función:
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
}
Dado que no hay un ámbito de bloqueo 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 se esperaba.
Solución 2015: para cada
Con la disponibilidad relativamente extendida del Array.prototype.forEach
(en 2015), vale la pena señalar que en aquellas situaciones que involucran iteración principalmente sobre una variedad de valores, .forEach()
proporciona una forma limpia y natural de obtener un cierre distinto para cada iteración Es decir, asumiendo 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:
var someArray = [ /* whatever */ ];
// ...
someArray.forEach(function(arrayElement) {
// ... code code code for this one element
someAsynchronousFunction(arrayElement, function() {
arrayElement.doSomething();
});
});
La idea es que cada invocación de la función de devolución de llamada utilizada con el bucle .forEach
será su propio cierre. El parámetro que se pasa a ese controlador es el elemento de la matriz específico para ese paso particular de la iteración. Si se usa en una devolución de llamada asíncrona, no chocará con ninguna de las otras devoluciones de llamada establecidas en otros pasos de la iteración.
Si está trabajando en jQuery, la función $.each()
le ofrece una capacidad similar.
Solución ES6: let
ECMAScript 6 (ES6), la versión más reciente de JavaScript, ahora está comenzando a implementarse en muchos navegadores de hoja perenne y sistemas backend. También hay transpilers como Babel que convertirán ES6 en ES5 para permitir el uso de nuevas funciones en sistemas más antiguos.
ES6 introduce nuevas palabras clave let
y const
que tienen un alcance diferente a las variables basadas en variables. Por ejemplo, en un bucle con un índice basado en let
, cada iteración a través del bucle tendrá un nuevo valor de i
en el que cada valor está dentro del bucle, por lo que su código funcionará como espera. Hay muchos recursos, pero recomendaría la publicación de bloqueo de alcance de 2ality como una gran fuente de información.
for (let i = 0; i < 3; i++) {
funcs[i] = function() {
console.log("My value: " + i);
};
}
Sin embargo, tenga en cuenta que IE9-IE11 y Edge antes de la compatibilidad con Edge 14 let
obtener el error anterior (no crean una nueva i
cada vez, por lo que todas las funciones anteriores registrarían 3 como si lo hiciéramos con var
). Edge 14 finalmente lo hace bien.
Con ES6 ahora ampliamente soportado, la mejor respuesta a esta pregunta ha cambiado. ES6 proporciona las palabras clave let
y const
para esta circunstancia exacta. En lugar de perder el tiempo con los cierres, podemos usar let
para establecer una variable de alcance de bucle como esta:
var funcs = [];
for (let i = 0; i < 3; i++) {
funcs[i] = function() {
console.log("My value: " + i);
};
}
val
apuntará entonces a un objeto que es específico para ese giro particular del bucle, y devolverá el valor correcto sin la notación de cierre adicional. Esto obviamente simplifica significativamente este problema.
const
es similar a let
con la restricción adicional de que el nombre de la variable no puede rebotar a una nueva referencia después de la asignación inicial.
El soporte del navegador ahora está aquí para aquellos que apuntan a las últimas versiones de los navegadores. Actualmente se admiten const
/ let
en las últimas versiones de 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/
Docs aquí:
Sin embargo, tenga en cuenta que IE9-IE11 y Edge antes de la compatibilidad con Edge 14 let
obtener el error anterior (no crean una nueva i
cada vez, por lo que todas las funciones anteriores registrarían 3 como si lo hiciéramos con var
). Edge 14 finalmente lo hace bien.
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 la cadena de alcance . Es la forma en que JavaScript resuelve una variable durante la ejecución.
- Cada definición de función forma un alcance que consiste en todas las variables locales declaradas por
var
y susarguments
. - Si tenemos una función interna definida dentro de otra función (externa), esto forma una cadena y se usará durante la ejecución
- Cuando se ejecuta una función, el tiempo de ejecución evalúa las variables buscando la cadena de alcance . Si una variable se puede encontrar en un cierto punto de la cadena, dejará de buscarla y la usará, de lo contrario continuará hasta que alcance el alcance global al que pertenece
window
.
En el código inicial:
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
Cuando funcs
se ejecuta, la cadena de alcance será function inner -> global
. Como la variable i
no se puede encontrar en function inner
(ni se declara usando var
ni se pasa como argumentos), continúa buscando, hasta que el valor de i
finalmente se encuentra en el ámbito global que es window.i
.
Al envolverlo en una función externa, defina explícitamente una función auxiliar como share hizo o use una función anónima como share hizo:
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
Cuando funcs
se ejecute, ahora la cadena de alcance será function inner -> function outer
. Este tiempo i
se puede encontrar en el alcance de la función externa, que se ejecuta 3 veces en el bucle for, cada vez que el valor está i
enlazado correctamente. No usará el valor de window.i
cuando se ejecute internamente.
Se pueden encontrar más detalles here
Incluye el error común al crear un cierre en el bucle como lo que tenemos aquí, así como por qué necesitamos un cierre y la consideración del rendimiento.
El problema principal con el código mostrado por el OP es que nunca se lee hasta el segundo bucle. Para demostrarlo, imagina ver un error dentro del código.
funcs[i] = function() { // and store them in funcs
throw new Error("test");
console.log("My value: " + i); // each should log its value.
};
El error en realidad no ocurre hasta que se funcs[someIndex]
()
. Usando esta misma lógica, debería ser evidente que el valor de i
tampoco se recopila hasta este punto. Una vez que finaliza el bucle original, i++
lleva a i
el valor de 3
que hace que la condición i < 3
falle y que el bucle finalice. En este punto, i
es 3
y, por lo tanto, cuando se funcs[someIndex]()
y se evalúa, es 3, cada vez.
Para superar esto, debe evaluar i
medida que se encuentra. Tenga en cuenta que esto ya ha ocurrido en la forma de funcs[i]
(donde hay 3 índices únicos). Hay varias formas de capturar este valor. Una es pasarlo como un 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í.
funcs[i] = new function() {
var closedVariable = i;
return function(){
console.log("My value: " + closedVariable);
};
};
Esto describe el error común con el uso de cierres en JavaScript.
Una función define un nuevo entorno.
Considerar:
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
Por cada vez que se invoca a makeCounter
, {counter: 0}
hace que se cree un nuevo objeto. Además, también se crea una nueva copia de obj
para hacer referencia al nuevo objeto. Por lo tanto, counter1
y counter2
son independientes entre sí.
Cierres en bucles
Usar un cierre en un bucle es complicado.
Considerar:
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
Observe que los counters[0]
y los counters[1]
no son independientes. De hecho, operan en el mismo obj
!
Esto se debe a que solo hay una copia de obj
compartida en todas las iteraciones del bucle, quizás por razones de rendimiento. Aunque {counter: 0}
crea un nuevo objeto en cada iteración, la misma copia de obj
solo se actualizará con una referencia al objeto más nuevo.
La solución es usar otra función auxiliar:
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);
}
}
Esto funciona porque a las variables locales en el alcance de la función directamente, así como a las variables de argumento de la función, se les asignan nuevas copias al ingresar.
Para una discusión detallada, vea las trampas de cierre de JavaScript y el uso
La solución más simple sería,
En lugar de usar:
var funcs = [];
for(var i =0; i<3; i++){
funcs[i] = function(){
alert(i);
}
}
for(var j =0; j<3; j++){
funcs[j]();
}
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:
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]();
}
La idea detrás de esto es encapsular todo el cuerpo del bucle for con un IIFE (Expresión de función inmediatamente invocada) y pasar new_i
como parámetro y capturarlo como i
. Dado que la función anónima se ejecuta inmediatamente, el valor i
es diferente para cada función definida dentro de la función anónima.
Esta solución parece ajustarse a cualquier problema de este tipo, 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!
Las funciones de JavaScript "cierran" el alcance al que tienen acceso después de la declaración, y conservan el acceso a ese alcance incluso cuando cambian las variables en ese alcance.
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]()
}
Cada función en la matriz anterior se cierra en el ámbito global (global, simplemente porque ese es el ámbito en el que están declarados).
Posteriormente, 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 están declaradas, y retienen el acceso a ese alcance incluso cuando los valores variables dentro de ese alcance cambian".
El uso de let
lugar de var
resuelve esto creando un nuevo ámbito cada vez que se ejecuta el bucle for
, creando un ámbito separado para que cada función se cierre. Varias otras técnicas hacen lo mismo con funciones adicionales.
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]()
}
( let
que las variables tengan un ámbito de bloque en lugar de un ámbito de función. Los bloques se indican con llaves, pero en el caso del bucle for, la variable de inicialización, i
en nuestro caso, se considera declarada entre llaves).
Lo que necesita comprender es que el alcance de las variables en javascript se basa en la función. Esta es una diferencia importante que, por ejemplo, c #, donde tiene un ámbito de bloque, y solo copiar la variable en una dentro del for for work funcionará.
Envolviéndolo en una función que evalúe devolver la función como 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 ámbito 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.
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]();
}
Otra forma de decirlo es que la i
en su función está vinculada al momento de ejecutar la función, no al momento de crear la función.
Cuando crea 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 brindan formas de solucionar el problema creando otra variable que no cambiará el valor para usted.
Solo pensé que añadiría una explicación para mayor claridad. Para una solución, personalmente, me gustaría ir con Harto, ya que es la forma más sencilla de hacerlo por las respuestas aquí. Cualquiera de los códigos publicados funcionará, pero optaría por una fábrica de cierres en lugar de tener que escribir un montón de comentarios para explicar por qué declaro una nueva variable (Freddy y 1800) o tengo una sintaxis de cierre incrustada (apphacker).
Otra forma que aún no se ha mencionado es el uso de 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]();
}
ACTUALIZAR
Como lo señalaron @squint y @mekdev, obtendrá un mejor rendimiento creando primero la función fuera del bucle y luego vinculando los resultados dentro del bucle.
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]();
}
Poco tarde en la fiesta, pero estaba explorando este tema hoy y noté que muchas de las respuestas no abordan completamente cómo Javascript trata los ámbitos, que es esencialmente a lo que se reduce esto.
Entonces, como muchos otros mencionaron, el problema es que la función interna está haciendo referencia a la misma variable i
. Entonces, ¿por qué no creamos una nueva variable local en cada iteración y tenemos la referencia de la función interna en su lugar?
//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 generó el último valor asignado a i
, ahora cada función interna solo genera el último valor asignado a ilocal
. Pero, ¿no debería cada iteración tener su propio ilocal
?
Resulta que ese es el problema. Cada iteración está compartiendo el mismo alcance, por lo que cada iteración después de la primera es simplemente sobrescribir ilocal
. Desde MDN :
Importante: JavaScript no tiene ámbito de bloque. Las variables introducidas con un bloque están sujetas a la función que contiene o al script, y los efectos de configurarlas persisten más allá del propio bloque. En otras palabras, las declaraciones de bloque no introducen un alcance. Si bien los bloques "independientes" tienen una sintaxis válida, no desea utilizar bloques independientes en JavaScript, porque no hacen lo que creen que hacen, si creen que hacen algo como los bloques en C o Java.
Reiterado por énfasis:
JavaScript no tiene alcance de bloque. Las variables introducidas con un bloque están sujetas a la función que contiene o al script
Podemos ver esto marcando 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. A pesar de que está redeclarando una variable, Javascript no lanzará un error y JSLint ni siquiera emitirá una advertencia. Esta es 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 las variables externas porque los ámbitos internos "encierran" los ámbitos externos.
Esto también significa que las funciones internas "sostienen" 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 envoltorio ilocal
para crear un nuevo alcance, declarar ilocal
en el nuevo alcance y devolver una función interna que usa 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 de envoltura otorga 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 envoltura, creamos una nueva función interna con su propio entorno separado, asegurando que las variables ilocal
no colisionen y se sobrescriban unas con otras. 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 mainstream, ahora podemos usar la nueva palabra clave let
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 qué fácil es ahora! Para obtener más información, consulte esta respuesta , en la que se basa mi información.
Tratar:
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]();
}
Editar (2014):
Personalmente creo que la respuesta más reciente de @ Aust sobre el uso de .bind
es la mejor manera de hacer este tipo de cosas ahora. También hay _.partial
lo-dash / guión _.partial
cuando no necesitas o quieres thisArg
.
Usando una expresión de función invocada de inmediato , la forma más sencilla y legible de incluir una variable de índice:
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);
}
Esto envía el iterador i
a la función anónima que definimos como index
. Esto crea un cierre, donde la variable i
se guarda para su uso posterior en cualquier funcionalidad asíncrona dentro del IIFE.
CONTADOR DE SER PRIMITIVO
Vamos a definir las funciones de devolución de llamada de la siguiente manera:
// ****************************
// COUNTER BEING A PRIMITIVE
// ****************************
function test1() {
for (var i=0; i<2; i++) {
setTimeout(function() {
console.log(i);
});
}
}
test1();
// 2
// 2
Una vez finalizado el tiempo de espera, se imprimirá 2 para ambos. Esto se debe a que la función de devolución de llamada accede al valor según el ámbito léxico , donde se definió la función.
Para pasar y conservar el valor mientras se definió la devolución de llamada, podemos crear un closure , para preservar el valor antes de que se invoque la devolución de llamada. Esto puede hacerse de la siguiente manera:
function test2() {
function sendRequest(i) {
setTimeout(function() {
console.log(i);
});
}
for (var i = 0; i < 2; i++) {
sendRequest(i);
}
}
test2();
// 1
// 2
Ahora, lo que es especial acerca de esto es "Las primitivas se pasan por valor y se copian. Así, cuando se define el cierre, mantienen el valor del bucle anterior".
CONTADOR DE SER UN OBJETO
Dado que los cierres tienen acceso a las variables de la función principal a través de la referencia, este enfoque diferiría del de las primitivas.
// ****************************
// COUNTER BEING AN OBJECT
// ****************************
function test3() {
var index = { i: 0 };
for (index.i=0; index.i<2; index.i++) {
setTimeout(function() {
console.log(''test3: '' + index.i);
});
}
}
test3();
// 2
// 2
Por lo tanto, incluso si se crea un cierre para la variable que se pasa como un objeto, el valor del índice de bucle no se conservará. Esto es para mostrar que los valores de un objeto no se copian, mientras que se accede a ellos por referencia.
function test4() {
var index = { i: 0 };
function sendRequest(index, i) {
setTimeout(function() {
console.log(''index: '' + index);
console.log(''i: '' + i);
console.log(index[i]);
});
}
for (index.i=0; index.i<2; index.i++) {
sendRequest(index, index.i);
}
}
test4();
// index: { i: 2}
// 0
// undefined
// index: { i: 2}
// 1
// undefined
Con las nuevas características de ES6 a nivel de bloque se gestiona el alcance:
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
}
El código en la pregunta de OP se reemplaza con en let
lugar de var
.
En primer lugar, entienda qué está mal con este código:
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
}
Aquí cuando la funcs[]
matriz se está inicializando, i
se está incrementando, la funcs
matriz se inicializa y el tamaño de la func
matriz se convierte en 3, por lo tanto i = 3,
. Ahora, cuando funcs[j]()
se llama a, se está utilizando de nuevo la variable i
, que ya se ha incrementado a 3.
Ahora para resolver esto, tenemos muchas opciones. A continuación se presentan dos de ellos:
Podemos inicializar
i
conlet
o inicializar una nueva variableindex
conlet
y que sea igual ai
. Por lo tanto, cuando se realice la llamada,index
se utilizará y su alcance finalizará después de la inicialización. Y para llamar,index
se inicializará de nuevo: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](); }
Otra opción puede ser introducir un
tempFunc
que devuelve la función real: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](); }
Tu código no funciona, porque lo que hace es:
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?
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 de i < 3
, se detiene inmediatamente cuando la condición es falsa, por lo que es i = 3
.
Debe comprender que, en el momento en que se crean las funciones, no se ejecuta ninguno de sus códigos, solo se guarda para más adelante. Y así, cuando se llaman más tarde, el intérprete los ejecuta y pregunta: "¿Cuál es el valor actual de i
?"
Por lo tanto, su objetivo es primero guardar el valor de la i
función y solo después de eso guardar la función en funcs
. Esto se podría hacer por ejemplo de esta manera:
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
}
De esta manera, cada función tendrá su propia variable x
y la estableceremos x
en el valor de i
en cada iteración.
Esta es solo una de las múltiples formas de resolver este problema.
Esta pregunta realmente muestra la historia de JavaScript! Ahora podemos evitar el alcance del bloque con las funciones de flecha y manejar los bucles directamente desde los nodos DOM usando métodos 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>
La razón por la que su ejemplo original no funcionó es que todos los cierres que creó en el bucle hicieron referencia al mismo marco. En efecto, tener 3 métodos en un objeto con una sola i
variable. Todos imprimieron el mismo valor.
Me sorprende que nadie haya sugerido aún el uso de la forEach
función para evitar (re) usar mejor las variables locales. De hecho, ya no uso for(var i ...)
nada por este motivo.
[0,2,3].forEach(function(i){ console.log(''My value:'', i); });
// My value: 0
// My value: 2
// My value: 3
// editado para usar en forEach
lugar de mapa.
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 la presente. 3-10 veces más rápido que el enlace dependiendo del navegador.
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);
}
}
Podría usar un módulo declarativo para listas de datos tales como query-js (*). En estas situaciones, personalmente encuentro un enfoque declarativo menos sorprendente.
var funcs = Query.range(0,3).each(function(i){
return function() {
console.log("My value: " + i);
};
});
Luego puede usar su segundo bucle y obtener el resultado esperado o puede hacer
funcs.iterate(function(f){ f(); });
(*) 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 por el enfoque declarativo :)
Prefiero usar la forEach
función, que tiene su propio cierre con la creación de un pseudo rango:
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
}
Eso parece más feo que los rangos en otros idiomas, pero en mi humilde opinión es menos monstruoso que otras soluciones.
Utilice la estructura de closure , esto reduciría su extra para bucle. Puedes hacerlo en un solo bucle for:
var funcs = [];
for (var i = 0; i < 3; i++) {
(funcs[i] = function() {
console.log("My value: " + i);
})(i);
}
Y aún otra solución: en lugar de crear otro bucle, simplemente enlaza la this
función de retorno.
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
}
Al unir esto, resuelve el problema también.
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
}