que - Cierre de JavaScript dentro de bucles-ejemplo práctico simple




que es un closure (20)

var funcs = [];
for (var i = 0; i < 3; i++) {      // let's create 3 functions
  funcs[i] = function() {          // and store them in funcs
    console.log("My value: " + i); // each should log its value.
  };
}
for (var j = 0; j < 3; j++) {
  funcs[j]();                      // and now let's run each one to see
}

Produce esto:

Mi valor: 3
Mi valor: 3
Mi valor: 3

Mientras que me gustaría que la salida:

Mi valor: 0
Mi valor: 1
Mi valor: 2

El mismo problema se produce cuando el retraso en la ejecución de la función se debe al uso de detectores de eventos:

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

… O código asíncrono, por ejemplo, utilizando promesas:

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

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

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


Vamos a comprobar, lo que realmente sucede cuando usted declara vary letuno por uno.

Caso 1 : utilizandovar

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

Ahora abra la ventana de la consola de Chrome presionando F12 y actualice la página. Gasta cada 3 funciones dentro de la matriz. Verás una propiedad llamada. [[Scopes]]Expande esa. Verá un objeto de matriz llamado "Global", expanda ese. Encontrará una propiedad 'i'declarada en el objeto que tiene valor 3.

Conclusión:

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

CASE2: usando let

Ahora reemplaza el 'var'con'let'

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

Haz lo mismo, ve a los ámbitos. Ahora verás dos objetos "Block"y "Global". Ahora expandir Blockobjeto, verá que 'i' está definido allí, y lo extraño es que, para cada función, el valor ies diferente (0, 1, 2).

Conclusión:

Cuando declara una variable usando 'let'incluso fuera de la función pero dentro del bucle, esta variable no será una variable global, se convertirá en una Blockvariable de nivel que solo está disponible para la misma función. Esa es la razón, estamos obteniendo valores de idiferentes para cada función cuando invocamos las funciones.

Para obtener más detalles acerca de cómo funciona más cerca, consulte el impresionante tutorial en video https://youtu.be/71AtaJpJHw0


prueba este mas corto

  • sin matriz

  • no extra para bucle


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

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

http://jsfiddle.net/7P6EN/


Aquí hay una solución simple que usa forEach (funciona de nuevo a IE9):

var funcs = [];
[0,1,2].forEach(function(i) {          // let's create 3 functions
    funcs[i] = function() {            // and store them in funcs
        console.log("My value: " + i); // each should log its value.
    };
})
for (var j = 0; j < 3; j++) {
    funcs[j]();                        // and now let's run each one to see
}

Huellas dactilares:

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

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

Solución clásica: Cierres.

Lo que desea hacer es vincular la variable dentro de cada función a un valor independiente e inalterable fuera de la función:

var funcs = [];

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

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

for (var j = 0; j < 3; j++) {
    funcs[j]();                        // and now let's run each one to see
}

Dado que no hay un ámbito de bloqueo en JavaScript (solo el alcance de la función) al envolver la creación de la función en una nueva función, se asegura de que el valor de "i" permanezca como se esperaba.

Solución 2015: para cada

Con la disponibilidad relativamente extendida del Array.prototype.forEach (en 2015), vale la pena señalar que en aquellas situaciones que involucran iteración principalmente sobre una variedad de valores, .forEach() proporciona una forma limpia y natural de obtener un cierre distinto para cada iteración Es decir, asumiendo que tiene algún tipo de matriz que contiene valores (referencias DOM, objetos, lo que sea), y surge el problema de configurar devoluciones de llamada específicas para cada elemento, puede hacer esto:

var someArray = [ /* whatever */ ];
// ...
someArray.forEach(function(arrayElement) {
  // ... code code code for this one element
  someAsynchronousFunction(arrayElement, function() {
    arrayElement.doSomething();
  });
});

La idea es que cada invocación de la función de devolución de llamada utilizada con el bucle .forEach será su propio cierre. El parámetro que se pasa a ese controlador es el elemento de la matriz específico para ese paso particular de la iteración. Si se usa en una devolución de llamada asíncrona, no chocará con ninguna de las otras devoluciones de llamada establecidas en otros pasos de la iteración.

Si está trabajando en jQuery, la función $.each() le ofrece una capacidad similar.

Solución ES6: let

ECMAScript 6 (ES6), la versión más reciente de JavaScript, ahora está comenzando a implementarse en muchos navegadores de hoja perenne y sistemas backend. También hay transpilers como Babel que convertirán ES6 en ES5 para permitir el uso de nuevas funciones en sistemas más antiguos.

ES6 introduce nuevas palabras clave let y const que tienen un alcance diferente a las variables basadas en variables. Por ejemplo, en un bucle con un índice basado en let , cada iteración a través del bucle tendrá un nuevo valor de i en el que cada valor está dentro del bucle, por lo que su código funcionará como espera. Hay muchos recursos, pero recomendaría la publicación de bloqueo de alcance de 2ality como una gran fuente de información.

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

Sin embargo, tenga en cuenta que IE9-IE11 y Edge antes de la compatibilidad con Edge 14 let obtener el error anterior (no crean una nueva i cada vez, por lo que todas las funciones anteriores registrarían 3 como si lo hiciéramos con var ). Edge 14 finalmente lo hace bien.


Después de leer varias soluciones, me gustaría agregar que la razón por la que esas soluciones funcionan es confiar en el concepto de la cadena de alcance . Es la forma en que JavaScript resuelve una variable durante la ejecución.

  • Cada definición de función forma un alcance que consiste en todas las variables locales declaradas por var y 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 la cadena de alcance . Si una variable se puede encontrar en un cierto punto de la cadena, dejará de buscarla y la usará, de lo contrario continuará hasta que alcance el alcance global al que pertenece window.

En el código inicial:

funcs = {};
for (var i = 0; i < 3; i++) {         
  funcs[i] = function inner() {        // function inner's scope contains nothing
    console.log("My value: " + i);    
  };
}
console.log(window.i)                  // test value 'i', print 3

Cuando funcsse ejecuta, la cadena de alcance será function inner -> global. Como 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 ifinalmente se encuentra en el ámbito global que es window.i.

Al envolverlo en una función externa, defina explícitamente una función auxiliar como share hizo o use una función anónima como share hizo:

funcs = {};
function outer(i) {              // function outer's scope contains 'i'
  return function inner() {      // function inner, closure created
   console.log("My value: " + i);
  };
}
for (var i = 0; i < 3; i++) {
  funcs[i] = outer(i);
}
console.log(window.i)          // print 3 still

Cuando 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 bucle for, cada vez que el valor está ienlazado correctamente. No usará el valor de window.icuando se ejecute internamente.

Se pueden encontrar más detalles here
Incluye el error común al crear un cierre en el bucle como lo que tenemos aquí, así como por qué necesitamos un cierre y la consideración del rendimiento.


El problema principal con el código mostrado por el OP es que nunca se lee hasta el segundo bucle. Para demostrarlo, imagina ver un error dentro del código.

funcs[i] = function() {            // and store them in funcs
    throw new Error("test");
    console.log("My value: " + i); // each should log its value.
};

El error en realidad no ocurre hasta que se funcs[someIndex] () . Usando esta misma lógica, debería ser evidente que el valor de i tampoco se recopila hasta este punto. Una vez que finaliza el bucle original, i++ lleva a i el valor de 3 que hace que la condición i < 3 falle y que el bucle finalice. En este punto, i es 3 y, por lo tanto, cuando se funcs[someIndex]() y se evalúa, es 3, cada vez.

Para superar esto, debe evaluar i medida que se encuentra. Tenga en cuenta que esto ya ha ocurrido en la forma de funcs[i] (donde hay 3 índices únicos). Hay varias formas de capturar este valor. Una es pasarlo como un parámetro a una función que ya se muestra de varias maneras aquí.

Otra opción es construir un objeto de función que pueda cerrarse sobre la variable. Eso se puede lograr así.

jsFiddle Demo

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

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

En lugar de usar:

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

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

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

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

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

La idea detrás de esto es encapsular todo el cuerpo del bucle for con un IIFE (Expresión de función inmediatamente invocada) y pasar new_i como parámetro y capturarlo como i . Dado que la función anónima se ejecuta inmediatamente, el valor i es diferente para cada función definida dentro de la función anónima.

Esta solución parece ajustarse a cualquier problema de este tipo, ya que requerirá cambios mínimos en el código original que sufre este problema. De hecho, esto es por diseño, ¡no debería ser un problema en absoluto!


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

var funcs = []

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

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

Cada función en la matriz anterior se cierra en el ámbito global (global, simplemente porque ese es el ámbito en el que están declarados).

Posteriormente, esas funciones se invocan registrando el valor más actual de i en el ámbito global. Esa es la magia, y la frustración, del cierre.

"Las funciones de JavaScript se cierran sobre el alcance en el que están declaradas, y retienen el acceso a ese alcance incluso cuando los valores variables dentro de ese alcance cambian".

El uso de let lugar de var resuelve esto creando un nuevo ámbito cada vez que se ejecuta el bucle for , creando un ámbito separado para que cada función se cierre. Varias otras técnicas hacen lo mismo con funciones adicionales.

var funcs = []

for (let i = 0; i < 3; i += 1) {
  funcs[i] = function () {
    console.log(i)
  }
}

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

( let que las variables tengan un ámbito de bloque en lugar de un ámbito de función. Los bloques se indican con llaves, pero en el caso del bucle for, la variable de inicialización, i en nuestro caso, se considera declarada entre llaves).


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

Cuando crea el cierre, i es una referencia a la variable definida en el ámbito externo, no una copia de la misma como era cuando creó el cierre. Será evaluado en el momento de la ejecución.

La mayoría de las otras respuestas brindan formas de solucionar el problema creando otra variable que no cambiará el valor para usted.

Solo pensé que añadiría una explicación para mayor claridad. Para una solución, personalmente, me gustaría ir con Harto, ya que es la forma más sencilla de hacerlo por las respuestas aquí. Cualquiera de los códigos publicados funcionará, pero optaría por una fábrica de cierres en lugar de tener que escribir un montón de comentarios para explicar por qué declaro una nueva variable (Freddy y 1800) o tengo una sintaxis de cierre incrustada (apphacker).


Otra forma que aún no se ha mencionado es el uso de Function.prototype.bind

var funcs = {};
for (var i = 0; i < 3; i++) {
  funcs[i] = function(x) {
    console.log('My value: ' + x);
  }.bind(this, i);
}
for (var j = 0; j < 3; j++) {
  funcs[j]();
}

ACTUALIZAR

Como lo señalaron @squint y @mekdev, obtendrá un mejor rendimiento creando primero la función fuera del bucle y luego vinculando los resultados dentro del bucle.

function log(x) {
  console.log('My value: ' + x);
}

var funcs = [];

for (var i = 0; i < 3; i++) {
  funcs[i] = log.bind(this, i);
}

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


Tratar:

var funcs = [];

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

Editar (2014):

Personalmente creo que la respuesta más reciente de @ Aust sobre el uso de .bind es la mejor manera de hacer este tipo de cosas ahora. También hay _.partial lo-dash / guión _.partial cuando no necesitas o quieres thisArg .


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

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

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

}

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


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

var funcs = [];
for (let i = 0; i < 3; i++) {          // let's create 3 functions
    funcs[i] = function() {            // and store them in funcs
        console.log("My value: " + i); // each should log its value.
    };
}
for (let j = 0; j < 3; j++) {
    funcs[j]();                        // and now let's run each one to see
}

El código en la pregunta de OP se reemplaza con en letlugar de var.


En primer lugar, entienda qué está mal con este código:

var funcs = [];
for (var i = 0; i < 3; i++) {          // let's create 3 functions
    funcs[i] = function() {            // and store them in funcs
        console.log("My value: " + i); // each should log its value.
    };
}
for (var j = 0; j < 3; j++) {
    funcs[j]();                        // and now let's run each one to see
}

Aquí cuando la funcs[]matriz se está inicializando, ise está incrementando, la funcsmatriz se inicializa y el tamaño de la funcmatriz se convierte en 3, por lo tanto i = 3,. Ahora, cuando funcs[j]()se llama a, se está utilizando de nuevo la variable i, que ya se ha incrementado a 3.

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

  1. Podemos inicializar icon leto inicializar una nueva variable indexcon lety que sea igual a i. Por lo tanto, cuando se realice la llamada, indexse utilizará y su alcance finalizará después de la inicialización. Y para llamar, indexse inicializará de nuevo:

    var funcs = [];
    for (var i = 0; i < 3; i++) {          
        let index = i;
        funcs[i] = function() {            
            console.log("My value: " + index); 
        };
    }
    for (var j = 0; j < 3; j++) {
        funcs[j]();                        
    }
    
  2. Otra opción puede ser introducir un tempFuncque devuelve la función real:

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

Esta pregunta realmente muestra la historia de JavaScript! Ahora podemos evitar el alcance del bloque con las funciones de flecha y manejar los bucles directamente desde los nodos DOM usando métodos Object.

const funcs = [1, 2, 3].map(i => () => console.log(i));
funcs.map(fn => fn())

const buttons = document.getElementsByTagName("button");
Object
  .keys(buttons)
  .map(i => buttons[i].addEventListener('click', () => console.log(i)));
<button>0</button><br>
<button>1</button><br>
<button>2</button>


La razón por la que su ejemplo original no funcionó es que todos los cierres que creó en el bucle hicieron referencia al mismo marco. En efecto, tener 3 métodos en un objeto con una sola ivariable. Todos imprimieron el mismo valor.


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 la presente. 3-10 veces más rápido que el enlace dependiendo del navegador.

var funcs = [];
for (var i = 0; i < 3; i++) {      // let's create 3 functions
  funcs[i] = curryShowValue(i);
}
for (var j = 0; j < 3; j++) {
  funcs[j]();                      // and now let's run each one to see
}

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

Ver la ganancia de rendimiento en diferentes navegadores .


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

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

Luego puede usar su segundo bucle y obtener el resultado esperado o puede hacer

funcs.iterate(function(f){ f(); });

(*) Soy el autor de query-js y, por lo tanto, estoy predispuesto a usarlo, así que no tome mis palabras como una recomendación para dicha biblioteca solo por el enfoque declarativo :)


Utilice la estructura de closure , esto reduciría su extra para bucle. Puedes hacerlo en un solo bucle for:

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

Y aún otra solución: en lugar de crear otro bucle, simplemente enlaza la 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 unir esto, resuelve el problema también.





closures