Cierre de JavaScript dentro de los bucles: ejemplo práctico simple


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

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 iny for ofbucles:

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?


55
¿Estás seguro de que no quieres funcsser una matriz, si estás usando índices numéricos? Solo un aviso.
DanMan

23
Este es un problema realmente confuso. Este artículo me ayuda a entenderlo . ¿Podría ayudar a otros también?
user3199690

44
Otra solución simple y explicada: 1) Las funciones anidadas tienen acceso al alcance "encima" de ellas ; 2) una solución de cierre ... "Un cierre es una función que tiene acceso al ámbito principal, incluso después de que la función principal se haya cerrado".
Peter Krauss

2
Consulte este enlace para mejorar javascript.info/tutorial/advanced-functions
Saurabh Ahuja

35
En ES6 , una solución trivial es declarar la variable i con let , que se aplica al cuerpo del bucle.
Tomas Nikodym

Respuestas:


2149

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:

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

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.forEachfunció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:

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 .forEachbucle 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 lety con constun alcance diferente al varde las variables basadas. Por ejemplo, en un bucle con un letíndice basado en, cada iteración a través del bucle tendrá una nueva variable icon 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.

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 Edge 14 admiten letpero se equivocan en lo anterior (no crean una nueva icada vez, por lo que todas las funciones anteriores registrarían 3 como lo harían si las usáramos var). Edge 14 finalmente lo hace bien.


77
¿ function createfunc(i) { return function() { console.log("My value: " + i); }; }Todavía no se cierra porque usa la variable i?
ア レ ッ ク ス

55
Desafortunadamente, esta respuesta está desactualizada y nadie verá la respuesta correcta en la parte inferior; el uso Function.bind()definitivamente es preferible ahora, consulte stackoverflow.com/a/19323214/785541 .
Wladimir Palant

81
@Wladimir: Su sugerencia de que .bind()es "la respuesta correcta" no es correcta. Cada uno tiene su propio lugar. Con .bind()usted no puede vincular argumentos sin vincular el thisvalor. También obtienes una copia del iargumento 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.
cookie monster

8
Deje de usar estos hacks de función de devolución, use [] .forEach o [] .map en su lugar porque evitan reutilizar las mismas variables de alcance.
Christian Landgren

32
@ChristianLandgren: Eso solo es útil si está iterando una matriz. Estas técnicas no son "hacks". Son conocimientos esenciales.

379

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.bind @ Aust sobre el uso es la mejor manera de hacer este tipo de cosas ahora. Hay también lo-dash / subrayado es _.partialcuando no se necesita o quiere meterse con bind's thisArg.


2
alguna explicación sobre el }(i));?
aswzen

3
@aswzen Creo que pasa icomo argumento indexde la función.
Jet Blue

en realidad está creando un índice de variable local.
Abhishek Singh

1
Invoque inmediatamente la expresión de la función, también conocida como IIFE. (i) es el argumento de la expresión de función anónima que se invoca inmediatamente y el índice se establece a partir de i.
Huevos

348

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, obtienes un mejor rendimiento creando primero la función fuera del ciclo y luego vinculando los resultados dentro 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]();
}


Esto es lo que hago en estos días también, también me gusta lo-dash / underscore's_.partial
Bjorn

17
.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.

55
@squint @mekdev - Ambos están en lo correcto. Mi ejemplo inicial fue escrito rápidamente para demostrar cómo bindse usa. He agregado otro ejemplo por sus sugerencias.
Agosto

55
Creo que en lugar de desperdiciar el cálculo en dos bucles O (n), simplemente haga para (var i = 0; i <3; i ++) {log.call (this, i); }
user2290820

1
.bind () hace lo que la respuesta aceptada sugiere MÁS con violines this.
niry

269

Usando una expresión de función invocada inmediatamente , la forma más simple y fácil de encerrar 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 ia la función anónima que definimos como index. Esto crea un cierre, donde la variable ise guarda para su uso posterior en cualquier funcionalidad asincrónica dentro del IIFE.


10
Para una mayor legibilidad del código y para evitar confusiones sobre cuál ies qué, cambiaría el nombre del parámetro de función index.
Kyle Falconer

55
¿Cómo usaría esta técnica para definir las funciones de matriz descritas en la pregunta original?
Nico

@Nico De la misma manera que se muestra en la pregunta original, excepto que usaría en indexlugar de 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
Caso particular de @Nico En OP, sólo están interactuando sobre los números, por lo que este no sería un gran caso para .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); }; });
JLRishe

164

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 ivariable. 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 ilocalantes 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.

Cierres

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 ilocalen 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 ilocalvariables 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 letpalabra 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.


También me gusta cómo explicaste el IIFE. Estaba buscando eso. Gracias.
CapturedTree

44
Ahora existe el alcance de bloque en JavaScript utilizando las palabras clave lety const. Si esta respuesta se ampliara para incluir eso, sería mucho más útil a nivel mundial en mi opinión.

@TinyGiant seguro, agregué información lety
vinculé

@ woojoo666 ¿Podría su respuesta también funcionar para llamar a dos URL alternativas en un bucle como este 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++ }:? (podría reemplazar window.open () con getelementbyid ......)
chiflado sobre natty

@nuttyaboutnatty perdón por una respuesta tan tardía. No parece que el código en su ejemplo ya funcione. No está utilizando isus funciones de tiempo de espera, por lo que no necesita un cierre
woojoo666

151

Con ES6 ahora ampliamente admitido, la mejor respuesta a esta pregunta ha cambiado. ES6 proporciona las palabras clave lety constpara esta circunstancia exacta. En lugar de perder el tiempo con los cierres, solo podemos usar letpara 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); 
    };
}

valluego 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.

constes similar a letla 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/ letactualmente 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 letpero se equivocan en lo anterior (no crean una nueva icada vez, por lo que todas las funciones anteriores registrarían 3 como lo harían si las usáramos var). Edge 14 finalmente lo hace bien.


Desafortunadamente, 'let' todavía no es totalmente compatible, especialmente en dispositivos móviles. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
MattC

2
A partir de junio '16, let es compatible con todas las versiones principales del navegador, excepto iOS Safari, Opera Mini y Safari 9. Los navegadores Evergreen lo admiten. Babel lo transpilará correctamente para mantener el comportamiento esperado sin el modo de alta conformidad activado.
Dan Pantry

@DanPantry, sí, ya es hora de una actualización :) Actualizado para reflejar mejor el estado actual de las cosas, incluida la adición de const, enlaces de documentos y una mejor información de compatibilidad.
Ben McCormick

¿No es por eso que usamos babel para transpilar nuestro código para que los navegadores que no admiten ES6 / 7 puedan entender lo que está sucediendo?
pixel 67

87

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).


71

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.

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 qué navegador? como dije, tiene problemas de compatibilidad, con eso quiero decir problemas de compatibilidad graves, como no creo que let sea compatible con IE.
eglasius el

1
@nickf sí, verifique esta referencia: developer.mozilla.org/En/New_in_JavaScript_1.7 ... revise la sección de definiciones de let, hay un ejemplo onclick dentro de un bucle
eglasius

2
@nickf hmm, en realidad tienes que especificar explícitamente la versión: <script type = "application / javascript; version = 1.7" /> ... No lo he usado en ningún lado debido a la restricción de IE, simplemente no lo es práctico :(
eglasius el

puede ver el soporte del navegador para las diferentes versiones aquí es.wikipedia.org/wiki/Javascript
eglasius el


59

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:

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

Tenga en cuenta que, sea cual sea la técnica que utilice, la indexvariable 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.


Gracias y tu solución funciona. Pero me gustaría preguntar por qué esto funciona, pero intercambiar la varlínea y la returnlínea no funcionaría. ¡Gracias!
midnite

@midnite Si intercambió vary returnluego la variable no se asignaría antes de devolver la función interna.
Boann

53

Esto describe el error común al usar 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 makeCounterse 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, counter1y counter2son 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

Tenga en cuenta que counters[0]y counters[1]son no independientes. De hecho, ¡operan de la misma manera obj!

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 copia objse 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 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.


Pequeña aclaración: en el primer ejemplo de cierres en bucles, los contadores [0] y los contadores [1] no son independientes, no por razones de rendimiento. La razón es que 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.
Charidimos

50

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 ies 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 una IIFE (Expresión de función invocada inmediatamente) y pasarla new_icomo un parámetro y capturarlo como i. 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!


2
Lee algo similar en un libro una vez. También prefiero esto, ya que no tiene que tocar su código existente (tanto) y resulta obvio por qué lo hizo, una vez que haya aprendido el patrón de la función de llamada automática: para atrapar esa variable en el recién creado alcance.
DanMan

1
@ Danman Gracias. Las funciones anónimas de auto llamada son una muy buena manera de lidiar con la falta de alcance variable de nivel de bloque de javascript.
Kemal Dağ

3
Auto-invocación, o auto-invocación no es el término apropiado para esta técnica, IIFE (Expresión de función invocada inmediatamente) es más exactamente. Ref: benalman.com/news/2010/11/…
jherax

31

prueba este más corto

  • sin matriz

  • sin extra para 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
Su solución parece salir correctamente, pero innecesariamente usa funciones, ¿por qué no solo console.log la salida? La pregunta original es sobre la creación de funciones anónimas que tienen el mismo cierre. El problema era que, dado que tienen un cierre único, el valor de i es el mismo para cada uno de ellos. Espero que lo hayas entendido.
Kemal Dağ

30

Aquí hay una solución simple que usa forEach(vuelve 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

27

El principal problema con el código que muestra el OP es que inunca se lee hasta el segundo bucle. Para demostrarlo, imagine 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 funcs[someIndex]se ejecuta (). Usando esta misma lógica, debería ser evidente que el valor de itampoco se recopila hasta este punto tampoco. Una vez que finaliza el ciclo original, i++lleva ial valor de lo 3cual resulta en la i < 3falla de la condición y el final del ciclo. En este punto, ies 3y cuando funcs[someIndex]()se usa, yi se evalúa, es 3, cada vez.

Para superar esto, debe evaluar icó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

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

23

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.

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 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 ien 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 letlugar de varresuelve esto creando un nuevo alcance cada vez que se forejecuta el bucle, creando un alcance separado para cada función para cerrar. 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]()
}

( lethace 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, ien nuestro caso, se considera declarada en las llaves).


1
Luché por entender este concepto hasta que leí esta respuesta. Toca un punto realmente importante: el valor de ise establece en el ámbito global. Cuando el forciclo termina de ejecutarse, el valor global de iahora es 3. Por lo tanto, cada vez que se invoca esa función en la matriz (usando, por ejemplo funcs[j]), la ifunción in hace referencia a la ivariable global (que es 3).
Modermo

13

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.

  • Cada definición de función forma un ámbito que consiste en todas las variables locales declaradas por vary sus arguments.
  • 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 en la cadena de alcance . Si se puede encontrar una variable en un determinado punto de la cadena, dejará de buscarla y usarla; 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 funcsse ejecuta, la cadena de alcance será function inner -> global. Como la variable ino se puede encontrar en function inner(ni declarada usando varni pasada como argumentos), continúa buscando, hasta que el valor de ifinalmente 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 :

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 funcsse ejecuta, ahora la cadena de alcance será function inner -> function outer. Este tiempo ise puede encontrar en el alcance de la función externa que se ejecuta 3 veces en el ciclo for, cada vez tiene un valor ienlazado 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.


Raramente escribimos este ejemplo de código en real, pero creo que sirve un buen ejemplo para entender lo fundamental. Una vez que tenemos el alcance en mente y cómo se encadenaron, queda más claro ver por qué 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 de forEach. Por lo tanto, cada función interna definida en la devolución de llamada podrá usar el elvalor correcto
wpding el

13

Con las nuevas características de ES6 se gestiona el alcance del nivel de bloque:

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 letlugar de var.


constproporciona el mismo resultado y debe usarse cuando el valor de una variable no cambiará. Sin embargo, el uso de constdentro 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 del letinterior del inicializador se implementa correctamente en Firefox, por lo que no debe preocuparse allí.

10

Me sorprende que nadie haya sugerido usar la forEachfunción para evitar (re) usar mejor las variables locales. De hecho, ya no estoy usando for(var i ...)nada por esta razón.

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

// editado para usar en forEachlugar de mapa.


3
.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.
JLRishe

Esta pregunta no se trata de bucle sobre una matriz
jherax

Bueno, él quiere crear una serie de funciones, este ejemplo muestra cómo hacerlo sin involucrar una variable global.
Christian Landgren

9

¡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.

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

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 ivariable. Todos imprimieron el mismo valor.


8

En primer lugar, entienda lo que 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 inicializa, ise incrementa, la funcsmatriz se inicializa y el tamaño de la funcmatriz se convierte en 3, entonces i = 3,. Ahora cuando funcs[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:

  1. Podemos inicializar icon leto inicializar una nueva variable indexcon lety que tenga la misma i. Entonces, cuando se realiza la llamada, indexse utilizará y su alcance finalizará después de la inicialización. Y para llamar, indexse inicializará nuevamente:

    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. Otra opción puede ser introducir una tempFuncque devuelva 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]();                        
    }

8

Utilice la estructura de cierre , esto reduciría su bucle adicional. Puedes hacerlo en un solo ciclo for:

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

7

Comprobaremos qué sucede realmente cuando declara vary let uno por uno.

Caso1 : usandovar

<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.

ingrese la descripción de la imagen aquí

ingrese la descripción de la imagen aquí

Conclusión:

  1. Cuando declara una variable usando 'var'fuera de una función, se convierte en una variable global (puede verificar escribiendo io window.ien la ventana de la consola, devolverá 3).
  2. La función anónima que declaró no llamará y verificará el valor dentro de la función a menos que invoque las funciones.
  3. Cuando invoca la función, console.log("My value: " + i)toma el valor de su Globalobjeto y muestra el resultado.

CASO2: usando let

Ahora reemplace 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á dos objetos "Block"y "Global". Ahora expanda el Blockobjeto, verá que 'i' está definido allí, y lo extraño es que, para cada función, el valor if ies diferente (0, 1, 2).

ingrese la descripción de la imagen aquí

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 una Blockvariable de nivel que solo está disponible para la misma función. Esa es la razón, estamos obteniendo el valor de idiferentes 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


4

Puede usar un módulo declarativo para listas de datos como query-js (*). En estas situaciones personalmente encuentro menos sorprendente un enfoque declarativo

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

Luego, podría usar su segundo ciclo y obtener el resultado esperado o podría 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 para el enfoque declarativo :)


1
Me encantaría una explicación del voto negativo. El código resuelve el problema en cuestión. Sería valioso saber cómo mejorar potencialmente el código
Rune FS

1
¿Qué es 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.
jherax

1
@jherax esas son, por supuesto, mejoras obvias. Gracias por el comentario. Podría haber jurado que ya había un enlace. Sin que la publicación fuera bastante inútil, supongo :). Mi idea inicial de mantenerlo fuera fue porque no estaba tratando de impulsar el uso de mi propia biblioteca sino más bien la idea declarativa. Sin embargo, en retrospectiva, estoy totalmente de acuerdo en que el enlace debería estar allí
Rune FS

4

Prefiero usar la forEachfunció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, menos monstruoso que otras soluciones.


¿Prefieres a qué? Esto parece ser un comentario en respuesta a alguna otra respuesta. No aborda la pregunta real en absoluto (ya que no está asignando una función, que se llamará más adelante, en cualquier lugar).
Quentin

Está relacionado exactamente con el problema mencionado: cómo iterar de forma segura sin problemas de cierre
Rax Wunter

Ahora no parece significativamente diferente de la respuesta aceptada.
Quentin

No. En la respuesta aceptada se sugiere usar "alguna matriz", pero tratamos con un rango en la respuesta, son cosas absolutamente diferentes, que desafortunadamente no tienen una buena solución en js, así que mi respuesta está tratando de resolver el problema de una manera buena y práctica
Rax Wunter

@Quentin, recomendaría investigar la solución antes de minusar
Rax Wunter

4

Y otra solución más: en lugar de crear otro bucle, simplemente vincula la thisfunció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 vincular esto , también se resuelve el problema.


3

Muchas soluciones parecen correctas, pero no mencionan que se llama, Curryingque 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.

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

Vea la ganancia de rendimiento en diferentes navegadores .


@TinyGiant El ejemplo con la función devuelta sigue curry optimizado para el rendimiento. No me subiría al carro de las funciones de flecha como todos los bloggers de JavaScript. Se ven geniales y limpios, pero promueven funciones de escritura en línea en lugar de usar funciones predefinidas. Esto puede ser una trampa no obvia en lugares calurosos. Otro problema es que no son solo azúcar sintáctico porque están ejecutando enlaces innecesarios creando así cierres de envoltura.
Pawel

2
Advertencia para futuros lectores: esta respuesta aplica incorrectamente el término Curry . "Curry es cuando desglosas una función que toma múltiples argumentos en una serie de funciones que forman parte de los argumentos". . Este código no hace nada por el estilo. Todo lo que has hecho aquí es tomar el código de la respuesta aceptada, mover algunas cosas, cambiar el estilo y nombrar un poco, luego llamarlo curry, que categóricamente no es así.

3

Su 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 icuando 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 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 ifuncionar y solo después guardar la función en funcs. Esto podría hacerse, 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 forma, cada función tendrá su propia variable xy estableceremos esto xen el valor de ien cada iteración.

Esta es solo una de las múltiples formas de resolver este 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

Use let (bloqueado-alcance) en lugar de 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]();                      
}

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.