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

3028

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:

My value: 3
My value: 3
My value: 3

Mientras que me gustaría que saliera:

My value: 0
My value: 1
My value: 2


El mismo problema ocurre cuando el retraso en la ejecución de la función se debe al uso de detectores 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));
}

También es evidente en for iny for ofbucles:

const arr = [1,2,3];
const fns = [];

for(var i in arr){
  fns.push(() => console.log(`index: ${i}`));
}

for(var v of arr){
  fns.push(() => console.log(`value: ${v}`));
}

for(var f of fns){
  f();
}

¿Cuál es la solución a este problema básico?

13
2292

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 ES6: let

ECMAScript 6 (ES6) introduce palabras clave nuevas lety constcuyo alcance es 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ía 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 son compatibles 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.


Solución ES5.1: forEach

Con la disponibilidad relativamente generalizada de la Array.prototype.forEachfunción (en 2015), vale la pena señalar que en aquellas situaciones que involucran 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, 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 .forEachbucle sea 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 clásica: cierres

Lo que quiere hacer es vincular la variable dentro de cada función a un valor independiente e invariable 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 alcance de 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.

25
  • 10
    no function createfunc(i) { return function() { console.log("My value: " + i); }; }sigue siendo el cierre porque usa la variable i? アレックス 28 de marzo de 2014 a las 3:45
  • 62
    Desafortunadamente, esta respuesta está desactualizada y nadie verá la respuesta correcta en la parte inferior; Function.bind()definitivamente es preferible usarla , consulte stackoverflow.com/a/19323214/785541 . Wladimir Palant 20 de junio de 2014 a las 12:21
  • 92
    @Wladimir: Tu sugerencia de que .bind()es "la respuesta correcta" no es correcta. Cada uno tiene su propio lugar. Con .bind()no puede vincular argumentos sin vincular el thisvalor. Además, obtiene una copia del iargumento sin la capacidad de mutarlo entre llamadas, lo que a veces es necesario. Entonces son construcciones bastante diferentes, sin mencionar que las .bind()implementaciones han sido históricamente lentas. Claro que en el ejemplo simple cualquiera funcionaría, pero los cierres son un concepto importante de entender, y de eso se trataba la pregunta. cookie monster 12/07/2014 a las 2:35
  • 11
    Deje de usar estos trucos de función de retorno, use [] .forEach o [] .map en su lugar porque evitan reutilizar las mismas variables de alcance. Christian Landgren 7 feb.15 a las 10:23
  • 42
    @ChristianLandgren: Eso solo es útil si está iterando un Array. Estas técnicas no son "trucos". Son conocimientos esenciales. user1106925 29/06/15 a las 16:31
402

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.

4
  • 3
    alguna explicación sobre el }(i));? aswzen 6 de abril de 2018 a las 1:32
  • 3
    @aswzen Creo que pasa icomo argumento indexde la función. Jet Blue 26/07/18 a las 22:01
  • en realidad, está creando un índice de variable local. Abhishek Singh 15/03/19 a las 15:17
  • 1
    Invocar inmediatamente la expresión de 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. Eggs 14/04/20 a las 5:51
374

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, se obtiene 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]();
}
7
  • Esto es lo que hago en estos días también, también me gustan los guiones bajos / subrayados_.partialBjorn 8 de diciembre de 2014 a las 5:18
  • 18
    .bind()será en gran parte obsoleto con las características de ECMAScript 6. Además, esto en realidad crea dos funciones por iteración. Primero el anónimo, luego el generado por .bind(). Un mejor uso sería crearlo fuera del bucle y luego .bind()dentro. user1106925 28 de junio de 2015 a las 3:29
  • 5
    @squint @mekdev - Ambos tienen razón. Mi ejemplo inicial se escribió rápidamente para demostrar cómo bindse usa. Agregué otro ejemplo según sus sugerencias. Aust 29/06/15 a las 16:23
  • 5
    Creo que en lugar de desperdiciar el cálculo en dos bucles O (n), solo hazlo para (var i = 0; i <3; i ++) {log.call (this, i); }user2290820 11/09/15 a las 12:14
  • 1
    .bind () hace lo que sugiere la respuesta aceptada PLUS juega con this. niry 8 de enero de 2017 a las 5:55
285

Usando una expresión de función invocada inmediatamente , la forma más sencilla y legible 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
  • 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 a index. Kyle Falconer 10 de enero de 2014 a las 16:45
  • 5
    ¿Cómo usaría esta técnica para definir las funciones de matriz descritas en la pregunta original? Nico 30/11/2014 a las 13:17
  • @Nico De la misma manera que se muestra en la pregunta original, excepto que usaría en indexlugar de i. JLRishe 31/03/15 a las 20:54
  • @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/04/15 a las 9:22
  • 1
    @Nico En el caso particular de OP, solo están iterando sobre números, por lo que este no sería un gran caso .forEach(), pero muchas veces, cuando uno comienza con una matriz, forEach()es una buena opción, como:var nums [4, 6, 7]; var funcs = {}; nums.forEach(function (num, i) { funcs[i] = function () { console.log(num); }; });JLRishe 1/04/15 a las 10:05
175

Un poco tarde para la fiesta, pero estaba explorando este problema 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.

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 hacemos que la función interna haga referencia a eso 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 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 lo suyo 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. Desde MDN :

Important: JavaScript does not have block scope. Variables introduced with a block are scoped to the containing function or script, and the effects of setting them persist beyond the block itself. In other words, block statements do not introduce a scope. Although "standalone" blocks are valid syntax, you do not want to use standalone blocks in JavaScript, because they don't do what you think they do, if you think they do anything like such blocks in C or Java.

Reiterado para enfatizar:

JavaScript does not have block scope. Variables introduced with a block are scoped to the containing function or script

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

Esta es exactamente la razón por la que 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 que 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.

Cierres

Esto también significa que las funciones internas "retienen" las variables externas y las mantienen vivas, incluso si la función externa regresa. Para utilizar esto, creamos y llamamos a una función contenedora simplemente para crear un nuevo alcance, declarar ilocalen 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 contenedora le da a la función interna un entorno privado al que solo ella puede acceder, un "cierre". Por lo tanto, cada vez que llamamos a la función contenedora, 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 dieron muchos otros usuarios de SO:

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

5
  • 6
    Ahora existe el alcance de bloques en JavaScript utilizando las palabras clave lety const. Si esta respuesta se expandiera para incluir eso, sería mucho más útil a nivel mundial en mi opinión. user4639281 27 de diciembre de 2017 a las 3:12
  • @TinyGiant seguro, agregué información lety vinculé una explicación más completawoojoo666 1 mar 18 a las 22:44
  • @ woojoo666 ¿Podría su respuesta también funcionar para llamar a dos URL alternas 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 ......)nutty about natty 14 de mayo de 2018 a las 19:08
  • @nuttyaboutnatty lo siento por una respuesta tan tardía. No parece que el código de su ejemplo ya funcione. No está utilizando ien sus funciones de tiempo de espera, por lo que no necesita un cierrewoojoo666 3 de junio de 2018 a las 22:58
  • Ups, lo siento, quería decir "parece que el código de tu ejemplo ya funciona"woojoo666 8 de junio de 2018 a las 11:22
163

Con ES6 ahora ampliamente soportado, la mejor respuesta a esta pregunta ha cambiado. ES6 proporciona las palabras clave lety constpara esta circunstancia exacta. En lugar de jugar con los cierres, 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 es 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 se puede volver a vincular a una nueva referencia después de la asignación inicial.

La compatibilidad con navegadores ahora está aquí para aquellos que se dirigen a las últimas versiones de navegadores. const/ letson actualmente compatibles con 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/

Documentos aquí:

Sin embargo, tenga en cuenta que IE9-IE11 y Edge antes de Edge 14 son compatibles 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.

4
  • Desafortunadamente, 'dejar' todavía no es totalmente compatible, especialmente en dispositivos móviles. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…MattC 23 feb 2016 a las 17:47
  • 3
    A partir de junio de 2016, let es compatible con todas las versiones principales de navegadores, 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 22/06/2016 a las 10:18
  • @DanPantry sí, ya es hora de una actualización :) Actualizado para reflejar mejor el estado actual de las cosas, incluida la adición de una mención de const, enlaces de documentos y una mejor información de compatibilidad. Ben McCormick 27/06/2016 a las 14:24
  • ¿No es por eso que usamos babel para transpilar nuestro código para que los navegadores que no son compatibles con ES6 / 7 puedan entender lo que está pasando? pixel 67 19/03/18 a las 15:56
94

Otra forma de decirlo es que ien su función está vinculado al momento de ejecutar la función, no al momento de crear la función.

Cuando crea el cierre, ies una referencia a la variable definida en el alcance externo, no una copia de la misma como era cuando creó el cierre. Se evaluará en el momento de la ejecución.

La mayoría de las otras respuestas brindan formas de solucionarlo 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 Harto, ya que es la forma más clara de hacerlo a partir de 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é estoy declarando una nueva variable (Freddy y 1800) o tengo una sintaxis de cierre incrustada extraña (apphacker).

0
77

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 copiar la variable a una dentro de for funcionará.

Envolverlo en una función que evalúe 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 del bloque. En ese caso, definir una variable dentro de for sería suficiente. 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]();
}
5
  • @nickf ¿qué navegador? como dije, tiene problemas de compatibilidad, con eso me refiero a problemas de compatibilidad graves, como no creo que let sea compatible con IE. eglasius 15 de abril de 2009 a las 6:54
  • 1
    @nickf sí, verifique esta referencia: developer.mozilla.org/En/New_in_JavaScript_1.7 ... verifique la sección de definiciones de let, hay un ejemplo de onclick dentro de un bucleeglasius 16 de abril de 2009 a las 2:55
  • 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 ninguna parte debido a la restricción de IE, simplemente no lo es práctico :(eglasius 16 de abril de 2009 a las 2:58
  • puede ver el soporte del navegador para las diferentes versiones aquí es.wikipedia.org/wiki/Javascripteglasius 16 de abril de 2009 a las 3:06
  • 4
    Consulte también ¿Qué navegadores admiten actualmente la palabra clave "dejar" de JavaScript? rds 15 de enero de 2013 a las 10:21
64

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 resultar muy útil.

2
  • Gracias y tu solución funciona. Pero me gustaría preguntar por qué funciona esto, pero cambiar la varlínea y la returnlínea no funcionaría. ¡Gracias! midnite 3 de diciembre de 2013 a las 2:56
  • @midnite Si intercambiaste vary returnluego la variable no se asignaría antes de que devolviera la función interna. Boann 3 de diciembre de 2013 a las 4:35
60

Esto describe el error común de 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

Cada vez que makeCounterse invoca, {counter: 0}se crea un nuevo objeto. Además, también 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 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 nocounters[1] son independientes. De hecho, ¡operan de la misma manera !obj

Esto se debe a que solo hay una copia de objcompartida 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 objsimplemente 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 del argumento de la función, se les asignan nuevas copias al ingresar.

1
  • Pequeña aclaración: en el primer ejemplo de cierres en bucles, los contadores [0] y los contadores [1] no son independientes no por motivos 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, dondequiera que ocurran, se procesan antes de que se ejecute cualquier código. Charidimos 21/09/19 a las 9:53
52

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 el bucle for 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 un IIFE (expresión de función invocada inmediatamente) y pasarlo new_icomo parámetro y capturarlo como i. Dado que la función anónima se ejecuta inmediatamente, el ivalor es diferente para cada función definida dentro de la función anónima.

Esta solución parece adaptarse 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!

3
  • 2
    Lea algo similar en un libro una vez. Yo también prefiero esto, ya que no tienes que tocar tu código existente (tanto) y se vuelve obvio por qué lo hiciste, una vez que hayas aprendido el patrón de función de auto-llamada: atrapar esa variable en el recién creado alcance. DanMan 26/07/2013 a las 11:18
  • 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ğ 26/07/2013 a las 12:20
  • 3
    Auto-llamarse o auto-invocarse no es el término apropiado para esta técnica, IIFE (expresión de función inmediatamente invocada) es más exactamente. Ref: benalman.com/news/2010/11/…jherax 27 oct 2015 a las 4:29
34

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
0
33

prueba este más corto

  • sin matriz

  • no extra para el bucle


for (var i = 0; i < 3; i++) {
    createfunc(i)();
}

function createfunc(i) {
    return function(){console.log("My value: " + i);};
}

http://jsfiddle.net/7P6EN/

1
  • 1
    Su solución parece tener una salida correcta pero usa funciones innecesariamente, ¿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 fue que, dado que tienen un solo cierre, el valor de i es el mismo para cada uno de ellos. Espero que lo tengas. Kemal Dağ 28/06/15 a las 8:51
30

El principal problema con el código que muestra el OP es que inunca se lee hasta el segundo ciclo. 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.
};

En realidad, el error no se produce 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. Una vez que finaliza el ciclo original, se i++lleva ial valor 3que da como resultado que la condición i < 3falle y el ciclo finalice. En este punto, ies 3y entonces cuando funcs[someIndex]()se usa y ise evalúa, es 3, cada vez.

Para superar esto, debe evaluar a imedida que se encuentra. Tenga en cuenta que esto ya ha sucedido en forma de funcs[i](donde hay 3 índices únicos). Hay varias formas de capturar este valor. Uno es pasarlo como parámetro a una función que ya se muestra de varias formas 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); 
    };
};
24

Las funciones de JavaScript "cierran" el alcance al que tienen acceso al momento de la declaración, y retienen el acceso a ese alcance incluso cuando las variables en ese alcance cambian.

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 están declaradas).

Posteriormente, 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 están declaradas y conservan el acceso a ese alcance incluso cuando los valores de las variables dentro de ese alcance cambian".

Usar en letlugar de varresuelve esto creando un nuevo alcance cada vez que se forejecuta el ciclo, creando un alcance 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]()
}

( lethace que las variables tengan un alcance de bloque. Los bloques se indican con llaves, pero en el caso del ciclo for, la variable de inicialización, ien nuestro caso, se considera declarada entre llaves).

1
  • 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, siempre que se invoca esa función en la matriz (usando, digamos funcs[j]), ien esa función hace referencia a la ivariable global (que es 3). Modermo 5 de abril de 2017 a las 2:50
15

Después de leer varias soluciones, me gustaría agregar que la razón por la que esas soluciones funcionan es para 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 consta de todas las variables locales declaradas por vary su 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 una variable se puede encontrar en un determinado punto de la cadena dejará de buscarla y la usará, de lo contrario continuará hasta alcanzar 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 ejecute, la cadena de alcance será function inner -> global. Dado que la variable ino se puede encontrar en function inner(ni se declara usando varni se pasa como argumentos), continúa buscando, hasta que el valor de ise encuentra finalmente en el alcance global que es window.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 ejecute, 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 ilimitado correctamente. No usará el valor de window.icuando se ejecuta 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.

1
  • Rara vez escribimos este ejemplo de código en realidad, pero creo que es un buen ejemplo para comprender lo fundamental. Una vez que tenemos el alcance en mente y cómo se encadenaron, es más claro ver por qué otras formas 'modernas' como Array.prototype.forEach(function callback(el) {})naturalmente funcionan: la devolución de llamada que se pasa naturalmente forma el alcance de envoltura con el correctamente enlazado en cada iteración de forEach. Entonces, cada función interna definida en la devolución de llamada podrá usar el elvalor correctowpding 26/04/2017 a las 14:19
13

Con las nuevas funciones de ES6, se gestiona el alcance a 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.

1
  • constproporciona el mismo resultado y debe usarse cuando el valor de una variable no cambie. Sin embargo, el uso de constdentro del inicializador del bucle for está implementado incorrectamente en Firefox y aún no se ha corregido. En lugar de declararse dentro del bloque, se declara fuera del bloque, lo que resulta en una redeclaración de la variable, que a su vez resulta en un error. El uso de letdentro del inicializador se implementa correctamente en Firefox, por lo que no hay necesidad de preocuparse por eso. user4639281 27 dic 2017 a las 3:05
10

Me sorprende que nadie haya sugerido todavía usar la forEachfunción para evitar (re) usar 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 forEachlugar de map.

3
  • 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 31/03/15 a las 19:59
  • Esta pregunta no se trata de un bucle sobre una matrizjherax 27/10/15 a las 4:14
  • Bueno, él quiere crear una matriz de funciones, este ejemplo muestra cómo hacerlo sin involucrar una variable global. Christian Landgren 11/11/15 a las 21:25
10

¡Esta pregunta realmente muestra la historia de JavaScript! Ahora podemos evitar el alcance de bloques con funciones de flecha y manejar 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>
9

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

8

En primer lugar, comprenda 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 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, vuelve a utilizar la variable i, que ya se ha incrementado a 3.

Ahora, para solucionar esto, tenemos muchas opciones. A continuación se muestran dos de ellos:

  1. Podemos inicializar icon leto inicializar una nueva variable indexcon lety hacerla igual a i. Entonces, cuando se realiza la llamada, indexse usará 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]();                        
    }
    
0
8

Use una estructura de cierre , esto reduciría su bucle extra. 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);
}
7

We will check , what actually happens when you declare var and let one by one.

Caso 1 : 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 su consola de Chrome presionando F12 y actualice la página. Gaste cada 3 funciones dentro de la matriz. Verá una propiedad llamada [[Scopes]].Expanda esa. Verá un objeto de matriz llamado "Global", expanda ese. Encontrará una propiedad 'i'declarada en el objeto que tiene el valor 3.

ingrese la descripción de la imagen aquí

ingrese la descripción de la imagen aquí

Conclusión:

  1. Cuando declaras una variable usando 'var'fuera de una función, se convierte en una variable global (puedes verificar escribiendo io window.ien la ventana de la consola; devolverá 3).
  2. La función annominous que declaró no llamará ni comprobará 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.

CASE2: 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 de 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 solamente. Esa es la razón, estamos obteniendo un valor de idiferente para cada función cuando invocamos las funciones.

Para obtener más detalles sobre cómo funciona el cerrador, 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 hacerlo

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

3
  • 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ódigoRune FS 18/06/15 a las 18:21
  • 1
    ¿Qué es Query.range(0,3)? Esto no forma parte de las etiquetas de esta pregunta. Además, si utiliza una biblioteca de terceros, puede proporcionar el enlace de la documentación. jherax 27 oct 2015 a las 4:07
  • 1
    @jherax esas son, por supuesto, mejoras obvias. Gracias por el comentario. Podría haber jurado que ya existía un vínculo. Sin eso, la publicación fue 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 completamente de acuerdo en que el enlace debería estar allíRune FS 27/10/15 a las 10:17
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.

5
  • ¿Lo prefieres a qué? Esto parece ser un comentario en respuesta a otra respuesta. No aborda la pregunta real en absoluto (ya que no está asignando una función, que se llamará más tarde, en cualquier lugar). Quentin 17/12/15 a las 14:24
  • Está relacionado exactamente con el problema mencionado: cómo iterar de forma segura sin problemas de cierreRax Wunter 17/12/15 a las 14:31
  • Ahora no parece significativamente diferente de la respuesta aceptada. Quentin 17/12/15 a las 14:31
  • 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, por lo que mi respuesta está tratando de resolver el problema de una manera buena y prácticaRax Wunter 17/12/15 a las 14:34
  • @Quentin Recomendaría investigar la solución antes de minuscarRax Wunter 17/12/15 a las 14:45
4

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

0
4

Hasta ES5, este problema solo se puede resolver mediante el cierre .

Pero ahora en ES6, tenemos variables de alcance a nivel de bloque. Cambiar var para dejar entrar primero al bucle for resolverá el problema.

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 (var j = 0; j < 3; j++) {
  funcs[j]();                      // and now let's run each one to see
}
4

Si tiene este tipo de problema con un whilebucle, en lugar de un forbucle, por ejemplo:

var i = 0;
while (i < 5) {
  setTimeout(function() {
    console.log(i);
  }, i * 1000);
  i++;
}

La técnica para cerrar sobre el valor actual es un poco diferente. Declare una variable de ámbito de bloque constdentro del whilebloque y asígnele la corriente i. Luego, siempre que la variable se use de forma asincrónica, reemplácela icon la nueva variable de ámbito de bloque:

var i = 0;
while (i < 5) {
  const thisIterationI = i;
  setTimeout(function() {
    console.log(thisIterationI);
  }, i * 1000);
  i++;
}

Para los navegadores más antiguos que no admiten variables de ámbito de bloque, puede usar un IIFE llamado con i:

var i = 0;
while (i < 5) {
  (function(innerI) {
    setTimeout(function() {
      console.log(innerI);
    }, innerI * 1000);
  })(i);
  i++;
}

Si la acción asincrónica que se invocará es setTimeoutcomo la anterior, también puede llamar setTimeoutcon un tercer parámetro para indicar el argumento para llamar a la función pasada con:

var i = 0;
while (i < 5) {
  setTimeout(
    (thisIterationI) => { // Callback
      console.log(thisIterationI);
    },
    i * 1000, // Delay
    i // Gets passed to the callback; becomes thisIterationI
  );
  i++;
}
2
  • Vale la pena señalar que la variable también se puede declarar usando en letlugar de const. Ambos permiten variables de ámbito de bloque. 3limin4t0r 22/09/19 a las 20:06
  • El IIFE es lo que estaba buscandoNoman_1 12 de febrero a las 10:12
3

Muchas soluciones parecen correctas, pero no mencionan su nombre, Curryingque es un patrón de diseño de programación funcional para situaciones como esta. 3-10 veces más rápido que enlazar 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 .

2
  • @TinyGiant El ejemplo con la función que se devuelve todavía está optimizado para el rendimiento. No me subiría al tren 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. Esta puede ser una trampa no obvia en lugares cálidos. Otro problema es que no son simplemente azúcar sintáctico porque están ejecutando enlaces innecesarios creando cierres de envoltura. Pawel 27 de diciembre de 2017 a las 1:52
  • 2
    Advertencia para futuros lectores: esta respuesta aplica incorrectamente el término Currying . "Currying es cuando descompone 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 ha hecho aquí es tomar el código de la respuesta aceptada, mover algunas cosas, cambiar el estilo y nombrar un poco, luego llamarlo currying, que categóricamente no es. user4639281 27 de diciembre de 2017 a las 2:36
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 i = 3.

Debe comprender que, en el momento en que se crean sus funciones, no se ejecuta ninguno de su código, solo se guarda para más adelante. Entonces, 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 guardar primero el valor de ipara funcionar y solo después de eso, 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 manera, cada función tendrá su propia variable xy establecemos esto xen el valor de ien cada iteración.

Esta es solo una de las múltiples formas de resolver este problema.