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


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
}

Emite 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 ocurre cuando la demora en ejecutar la función es causada por el uso de detectores de eventos:

var buttons = document.getElementsByTagName("button");
for (var i = 0; i < buttons.length; i++) {          // let's create 3 functions
  buttons[i].addEventListener("click", function() { // as event listeners
    console.log("My value: " + i);                  // each should log its value.
  });
}
<button>0</button><br>
<button>1</button><br>
<button>2</button>

... o código asíncrono, p. usando Promesas:

// Some async wait function
const wait = (ms) => new Promise((resolve, reject) => setTimeout(resolve, ms));

for(var i = 0; i < 3; i++){
  wait(i * 100).then(() => console.log(i)); // Log `i` as soon as each promise resolves.
}

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


2317
2018-04-15 06:06


origen


Respuestas:


Bueno, el problema es que la variable i, dentro de cada una de sus funciones anónimas, está ligada a la misma variable fuera de la función.

Solución clásica: cierres

Lo que quiere 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++) {
    funcs[j]();                        // and now let's run each one to see
}

Dado que no hay alcance de bloque en el ámbito de función de JavaScript, al envolver la creación de función en una nueva función, se asegura de que el valor de "i" permanezca como lo desea.


Solución 2015: para cada uno

Con la disponibilidad relativamente generalizada de Array.prototype.forEach función (en 2015), vale la pena señalar que en aquellas situaciones que implican iteración principalmente sobre una matriz de valores, .forEach() proporciona una forma limpia y natural de obtener un cierre distintivo 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 .forEach loop 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 utiliza 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 trabaja en jQuery, el $.each() función le da una capacidad similar.


Solución ES6: let

ECMAScript 6 (ES6), la versión más nueva de JavaScript, ahora se está empezando a implementar en muchos navegadores y sistemas back-end de hoja perenne. También hay transpilers como Babel que convertirá ES6 a ES5 para permitir el uso de nuevas funciones en sistemas antiguos.

ES6 presenta nuevo let y const palabras clave que tienen un alcance diferente al de varvariables basadas en Por ejemplo, en un bucle con un letbasado en el índice, cada iteración a través del ciclo tendrá un nuevo valor de i donde cada valor se delimita dentro del ciclo, para que su código funcione como espera. Hay muchos recursos, pero recomendaría 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 la compatibilidad con Edge 14 let pero haz lo anterior mal (no crean un nuevo i cada vez, por lo que todas las funciones anteriores registrarían 3 como lo harían si usáramos var) Edge 14 finalmente lo hace bien.


1795
2018-04-15 06:18



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 @ Aust es respuesta más reciente sobre el uso .bind es la mejor manera de hacer este tipo de cosas ahora. También hay lo-dash / underscore's _.partial cuando no necesitas o quieres jugar con bindes thisArg.


340
2018-04-15 06:10



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 señalan @squint y @mekdev, se obtiene un mejor rendimiento al crear primero la función fuera del bucle y luego enlazar 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]();
}


310
2017-10-11 16:41



Usando un Expresión de función invocada inmediatamente, la forma más sencilla y legible de incluir una variable de índice:

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

    (function(index) {
        console.log('iterator: ' + index);
        //now you can also loop an ajax call here 
        //without losing track of the iterator value: $.ajax({});
    })(i);

}

Esto envía el iterador i en la función anónima de la cual definimos como index. Esto crea un cierre, donde la variable i se guarda para su uso posterior en cualquier funcionalidad asincrónica dentro de IIFE.


234
2017-10-11 18:23



Un poco tarde para la fiesta, pero estaba explorando este tema hoy y me di cuenta de que muchas de las respuestas no abordan por completo cómo trata Javascript los ámbitos, que es esencialmente a lo que se reduce esto.

Como muchos otros mencionaron, el problema es que la función interna hace referencia al mismo i variable. Entonces, ¿por qué no creamos una nueva variable local en cada iteración y, en su lugar, hacemos referencia a la función interna?

//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 a i, ahora cada función interna solo emite el último valor asignado a ilocal. Pero no debería cada iteración tener su propio ilocal?

Resulta que ese es el problema. Cada iteración comparte el mismo alcance, por lo que cada iteración después de la primera está sobrescribiendo 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 instrucciones de bloque no introducen un alcance. Aunque los bloques "independientes" son sintaxis válidos, no desea usar bloques independientes en JavaScript, porque no hacen lo que cree que hacen, si cree que hacen algo parecido a dichos bloques en C o Java.

Reiterado para enfatizar:

JavaScript no tiene alcance de bloque. Las variables introducidas con un bloque tienen un alcance para la función que lo contiene o la secuencia de comandos

Podemos ver esto comprobando ilocal antes de declararlo en cada iteración:

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (var i = 0; i < 3; i++) {
  console.log(ilocal);
  var ilocal = i;
}

Esto es exactamente por qué este error es tan complicado. Aunque está redeclarando una variable, Javascript no arrojará un error, y JSLint ni siquiera arrojará 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 variables externas porque los ámbitos internos "encierran" ámbitos externos.

Closures

Esto también significa que las funciones internas "se aferran" a las variables externas y las mantienen con vida, incluso si la función externa regresa. Para utilizar esto, creamos y llamamos a una función de contenedor simplemente para crear un nuevo ámbito, declarar ilocalen el nuevo alcance, y devuelve 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 de envoltura 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 envoltura creamos una nueva función interna con su propio entorno separado, asegurando que la ilocal las variables no colisionan y sobrescriben 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 integrado, ahora podemos usar el nuevo let palabra clave para crear variables de ámbito de bloque:

//overwrite console.log() so you can see the console output
console.log = function(msg) {document.body.innerHTML += '<p>' + msg + '</p>';};

var funcs = {};
for (let i = 0; i < 3; i++) { // use "let" to declare "i"
    funcs[i] = function() {
        console.log("My value: " + i); //each should reference its own local variable
    };
}
for (var j = 0; j < 3; j++) { // we can use "var" here without issue
    funcs[j]();
}

¡Mira qué fácil es ahora! Para más información, ver esta respuesta, de lo cual se basa mi información.


127
2018-04-10 09:57



Con ES6 ahora ampliamente respaldado, la mejor respuesta a esta pregunta ha cambiado. ES6 proporciona el let y const palabras clave para esta circunstancia exacta. En lugar de meternos con cierres, podemos simplemente usar let para establecer una variable de ámbito de bucle como esta:

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

val luego apuntará a un objeto que sea específico para ese giro particular del ciclo, y devolverá el valor correcto sin la notación de cierre adicional. Esto obviamente simplifica significativamente este problema.

const es parecido a let con la restricción adicional de que el nombre de la variable no puede recuperarse a una nueva referencia después de la asignación inicial.

El soporte para navegadores ahora está aquí para aquellos que apuntan a las últimas versiones de navegadores. const/let actualmente son compatibles con los últimos Firefox, Safari, Edge y Chrome. También es compatible con Node, y puede usarlo en cualquier lugar aprovechando las herramientas de compilación como Babel. Puedes 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 la compatibilidad con Edge 14 let pero haz lo anterior mal (no crean un nuevo i cada vez, por lo que todas las funciones anteriores registrarían 3 como lo harían si usáramos var) Edge 14 finalmente lo hace bien.


121
2018-05-21 03:04



Otra forma de decirlo es que el i en su función está vinculada en el momento de la ejecución de la función, no en el 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 tal 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 evitar la creación de otra variable que no cambiará el valor para usted.

Solo pensé en agregar una explicación para la claridad. Para una solución, personalmente, me gustaría ir con Harto, ya que es la forma más fácil de explicarlo de las respuestas aquí. Cualquiera de los códigos publicados funcionará, pero optaría por una fábrica de cierre antes de tener que escribir una pila de comentarios para explicar por qué estoy declarando una nueva variable (Freddy y 1800) o tengo una extraña sintaxis de cierre integrado (apphacker).


77
2018-04-15 06:48



Lo que necesita comprender es que el alcance de las variables en javascript se basa en la función. Esta es una diferencia importante que decir c # donde tienes el alcance del bloque, y simplemente copiar la variable a uno dentro de for funcionará.

Envolverlo en una función que evalúa la devolución de la función como la respuesta de aplanadora hará el truco, ya que la variable ahora tiene el alcance de la función.

También hay una palabra clave let en lugar de var, que permitiría usar la regla de ámbito de bloque. En ese caso, definir una variable dentro del 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]();                        
}

61
2018-04-15 06:25



Aquí hay otra variación de la técnica, similar a Bjorn (apphacker), que le permite asignar el valor de la variable dentro de la función en lugar de pasarlo como un parámetro, que a veces puede ser más claro:

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

Tenga en cuenta que cualquiera que sea la técnica que use, la index la variable se convierte en un tipo 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.


49
2017-08-07 08:45



Esto describe el error común con el uso de cierres en JavaScript.

Una función define un nuevo entorno

Considerar:

function makeCounter()
{
  var obj = {counter: 0};
  return {
    inc: function(){obj.counter ++;},
    get: function(){return obj.counter;}
  };
}

counter1 = makeCounter();
counter2 = makeCounter();

counter1.inc();

alert(counter1.get()); // returns 1
alert(counter2.get()); // returns 0

Por cada tiempo makeCounter es invocado, {counter: 0} da como resultado un nuevo objeto que se está creando. Además, una nueva copia de obj  se crea también para hacer referencia al nuevo objeto. Así, counter1 y counter2 son independientes el uno del otro.

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

Darse cuenta de counters[0] y counters[1] son no independiente. De hecho, operan en el mismo obj!

Esto se debe a que solo hay una copia de obj compartido en todas las iteraciones del ciclo, tal vez por motivos de rendimiento. Aunque {counter: 0} crea un nuevo objeto en cada iteración, la misma copia de obj se actualizará con un 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 función, se asignan nuevas copias al ingresar.

Para una discusión detallada, por favor vea Errores de cierre de JavaScript y uso


45
2018-04-20 09:59



La solución más simple sería,

En lugar de usar:

var funcs = [];
for(var i =0; i<3; i++){
    funcs[i] = function(){
        alert(i);
    }
}

for(var j =0; j<3; j++){
    funcs[j]();
}

que alerta "2", por 3 veces. Esto se debe a que las funciones anónimas creadas en for loop, comparten el mismo cierre, y en ese cierre, el valor de i es el mismo. Use esto para evitar el cierre compartido:

var funcs = [];
for(var new_i =0; new_i<3; new_i++){
    (function(i){
        funcs[i] = function(){
            alert(i);
        }
    })(new_i);
}

for(var j =0; j<3; j++){
    funcs[j]();
}

La idea detrás de esto es, encapsulando todo el cuerpo del bucle for con un IIFE (Expresión de función invocada inmediatamente) y pasar new_i como un parámetro y capturarlo como i. Dado que la función anónima se ejecuta inmediatamente, el i el 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!


41
2018-06-25 14:21