Pregunta Cierres JavaScript vs. funciones anónimas


Un amigo mío y yo estamos discutiendo qué es un cierre en JS y qué no. Solo queremos asegurarnos de que realmente lo comprendamos correctamente.

Tomemos este ejemplo. Tenemos un ciclo de conteo y queremos imprimir la variable de contador en la consola con retraso. Por lo tanto usamos setTimeout y cierres para capturar el valor de la variable de contador para asegurarse de que no imprima N veces el valor N.

La solución incorrecta sin cierres o algo cercano a cierres sería:

for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

que por supuesto imprimirá 10 veces el valor de i después del ciclo, concretamente 10.

Entonces su intento fue:

for(var i = 0; i < 10; i++) {
    (function(){
        var i2 = i;
        setTimeout(function(){
            console.log(i2);
        }, 1000)
    })();
}

imprimiendo de 0 a 9 como se esperaba.

Le dije que él no está usando un cierre capturar i, pero él insiste en que lo es. Probé que él no usa cierres poniendo el cuerpo de bucle for dentro de otro setTimeout (pasando su función anónima a setTimeout), imprimiendo 10 veces 10 nuevamente. Lo mismo aplica si almaceno su función en var y ejecutarlo después el ciclo, también imprime 10 veces 10. Entonces mi argumento es que él realmente no capturar El valor de i, haciendo su versión no un cierre.

Mi intento fue:

for(var i = 0; i < 10; i++) {
    setTimeout((function(i2){
        return function() {
            console.log(i2);
        }
    })(i), 1000);
}

Entonces capturo i (llamado i2 dentro del cierre), pero ahora yo regreso otra función y pase esto. En mi caso, la función pasada a setTimeout realmente captura i.

¿Quién está usando cierres y quién no?

Tenga en cuenta que ambas soluciones imprimen de 0 a 9 en la consola con retraso, por lo que resuelven el problema original, pero queremos entender cuál de esas dos soluciones usa cierres para lograr esto.


523
2017-10-17 08:36


origen


Respuestas:


Nota del editor: Todas las funciones en JavaScript son cierres como se explica en este enviar. Sin embargo, solo estamos interesados ​​en identificar un subconjunto de estas funciones que son interesante desde un punto de vista teórico. A partir de ahora cualquier referencia a la palabra cierre se referirá a este subconjunto de funciones a menos que se indique lo contrario.

Una explicación simple para los cierres:

  1. Toma una función. Llamémoslo F.
  2. Listar todas las variables de F.
  3. Las variables pueden ser de dos tipos:
    1. Variables locales (variables vinculadas)
    2. Variables no locales (variables libres)
  4. Si F no tiene variables libres, entonces no puede ser un cierre.
  5. Si F tiene alguna variable libre (que se define en un alcance principal de F) luego:
    1. Debe haber solo un alcance principal de F para el cual un variable libre está vinculada.
    2. Si F es referenciado desde afuera ese alcance de los padres, luego se convierte en un cierre para ese variable libre.
    3. Ese variable libre se llama un upvalue del cierre F.

Ahora usemos esto para descubrir quién usa cierres y quién no (por razones de explicación he nombrado las funciones):

Caso 1: El programa de tu amigo

for (var i = 0; i < 10; i++) {
    (function f() {
        var i2 = i;
        setTimeout(function g() {
            console.log(i2);
        }, 1000);
    })();
}

En el programa anterior, hay dos funciones: f y g. Veamos si son cierres:

por f:

  1. Listar las variables:
    1. i2 es un local variable.
    2. i es un gratis variable.
    3. setTimeout es un gratis variable.
    4. g es un local variable.
    5. console es un gratis variable.
  2. Encuentre el ámbito primario al que está vinculada cada variable libre:
    1. i es ligado al alcance global.
    2. setTimeout es ligado al alcance global.
    3. console es ligado al alcance global.
  3. En qué ámbito está la función referenciado? los alcance global.
    1. Por lo tanto i no es cerrado sobre por f.
    2. Por lo tanto setTimeout no es cerrado sobre por f.
    3. Por lo tanto console no es cerrado sobre por f.

Por lo tanto, la función f no es un cierre.

por g:

  1. Listar las variables:
    1. console es un gratis variable.
    2. i2 es un gratis variable.
  2. Encuentre el ámbito primario al que está vinculada cada variable libre:
    1. console es ligado al alcance global.
    2. i2 es ligado al alcance de f.
  3. En qué ámbito está la función referenciado? los ámbito de aplicación de setTimeout.
    1. Por lo tanto console no es cerrado sobre por g.
    2. Por lo tanto i2 es cerrado sobre por g.

Por lo tanto, la función g es un cierre para la variable gratuita i2 (que es un valor superior para g) cuando sus referenciado desde adentro setTimeout.

Mal por tí: Tu amigo está usando un cierre. La función interna es un cierre.

Caso 2: tu programa

for (var i = 0; i < 10; i++) {
    setTimeout((function f(i2) {
        return function g() {
            console.log(i2);
        };
    })(i), 1000);
}

En el programa anterior, hay dos funciones: f y g. Veamos si son cierres:

por f:

  1. Listar las variables:
    1. i2 es un local variable.
    2. g es un local variable.
    3. console es un gratis variable.
  2. Encuentre el ámbito primario al que está vinculada cada variable libre:
    1. console es ligado al alcance global.
  3. En qué ámbito está la función referenciado? los alcance global.
    1. Por lo tanto console no es cerrado sobre por f.

Por lo tanto, la función f no es un cierre.

por g:

  1. Listar las variables:
    1. console es un gratis variable.
    2. i2 es un gratis variable.
  2. Encuentre el ámbito primario al que está vinculada cada variable libre:
    1. console es ligado al alcance global.
    2. i2 es ligado al alcance de f.
  3. En qué ámbito está la función referenciado? los ámbito de aplicación de setTimeout.
    1. Por lo tanto console no es cerrado sobre por g.
    2. Por lo tanto i2 es cerrado sobre por g.

Por lo tanto, la función g es un cierre para la variable gratuita i2 (que es un valor superior para g) cuando sus referenciado desde adentro setTimeout.

Bien por usted: Estás usando un cierre. La función interna es un cierre.

Entonces, tú y tu amigo están usando cierres. Deja de discutir. Espero haber aclarado el concepto de cierres y cómo identificarlos para ustedes dos.

Editar: Una explicación simple de por qué se cierran todas las funciones (créditos @ Peter):

Primero consideremos el siguiente programa (es el controlar)

lexicalScope();

function lexicalScope() {
    var message = "This is the control. You should be able to see this message being alerted.";

    regularFunction();

    function regularFunction() {
        alert(eval("message"));
    }
}

  1. Sabemos que ambos lexicalScope y regularFunction no son cierres de la definición anterior.
  2. Cuando ejecutamos el programa esperamos  message ser alertado porque  regularFunction no es un cierre (es decir, tiene acceso a todas las variables en su ámbito principal, incluido message)
  3. Cuando ejecutamos el programa observamos ese message de hecho está alerta.

A continuación, consideremos el siguiente programa (es el alternativa)

var closureFunction = lexicalScope();

closureFunction();

function lexicalScope() {
    var message = "This is the alternative. If you see this message being alerted then in means that every function in JavaScript is a closure.";

    return function closureFunction() {
        alert(eval("message"));
    };
}

  1. Sabemos que solo closureFunction es un cierre de la definición anterior.
  2. Cuando ejecutamos el programa esperamos  message no ser alertado porque  closureFunction es un cierre (es decir, solo tiene acceso a todo su variables no locales a el momento en que se crea la función (ver esta respuesta) - esto no incluye message)
  3. Cuando ejecutamos el programa observamos ese message en realidad está siendo alertado.

¿Qué inferimos de esto?

  1. Los intérpretes de JavaScript no tratan los cierres de forma diferente a la forma en que tratan otras funciones.
  2. Cada función lleva su cadena de alcance junto con eso. Los cierres no tienen una separar entorno de referencia.
  3. Un cierre es como cualquier otra función. Simplemente los llamamos cierres cuando están referenciado en un alcance fuera de el alcance al que pertenecen porque Este es un caso interesante.

617
2017-10-17 10:01



De acuerdo con la closure definición:

Un "cierre" es una expresión (típicamente una función) que puede tener variables libres junto con un ambiente que une esas variables (que "cierra" la expresión).

Tu estas usando closure si define una función que usa una variable que está definida fuera de la función. (llamamos a la variable a variable libre)
Todos usan closure(incluso en el primer ejemplo).


91
2017-10-17 08:59



En una palabra Cierres Javascript permitir una función a acceder a una variable es decir declarado en una función de padre léxico.

Veamos una explicación más detallada. Para comprender los cierres, es importante entender cómo JavaScript abarca las variables.

Alcances

En JavaScript, los ámbitos se definen con funciones. Cada función define un nuevo alcance.

Considera el siguiente ejemplo;

function f()
{//begin of scope f
  var foo='hello'; //foo is declared in scope f
  for(var i=0;i<2;i++){//i is declared in scope f
     //the for loop is not a function, therefore we are still in scope f
     var bar = 'Am I accessible?';//bar is declared in scope f
     console.log(foo);
  }
  console.log(i);
  console.log(bar);
}//end of scope f

llamando f impresiones

hello
hello
2
Am I Accessible?

Consideremos ahora el caso en que tenemos una función g definido dentro de otra función f.

function f()
{//begin of scope f
  function g()
  {//being of scope g
    /*...*/
  }//end of scope g
  /*...*/
}//end of scope f

Llamaremos f el padre léxico de g. Como se explicó anteriormente, ahora tenemos 2 ámbitos; el alcance f y el alcance g.

Pero un alcance está "dentro" del otro ámbito, ¿es el alcance de la función hijo parte del alcance de la función principal? ¿Qué sucede con las variables declaradas en el alcance de la función padre? ¿podré acceder a ellos desde el alcance de la función hija? Ahí es exactamente donde intervienen los cierres.

Cierres

En JavaScript, la función g no solo puede acceder a cualquier variable declarada en el alcance g pero también acceder a cualquier variable declarada en el alcance de la función padre f.

Considera seguir;

function f()//lexical parent function
{//begin of scope f
  var foo='hello'; //foo declared in scope f
  function g()
  {//being of scope g
    var bar='bla'; //bar declared in scope g
    console.log(foo);
  }//end of scope g
  g();
  console.log(bar);
}//end of scope f

llamando f impresiones

hello
undefined

Miremos la línea console.log(foo);. En este punto, estamos en el alcance g e intentamos acceder a la variable foo eso está declarado en el alcance f. Pero como se dijo anteriormente, podemos acceder a cualquier variable declarada en una función parental léxica, que es el caso aquí; g es el padre léxico de f. Por lo tanto hello está impreso.
Veamos ahora la línea console.log(bar);. En este punto, estamos en el alcance f e intentamos acceder a la variable bar eso está declarado en el alcance g. bar no está declarado en el alcance actual y la función g no es el padre de f, por lo tanto bar es indefinido

En realidad, también podemos acceder a las variables declaradas en el alcance de una función léxica de "gran padre". Por lo tanto, si hubiera una función h definido dentro de la función g

function f()
{//begin of scope f
  function g()
  {//being of scope g
    function h()
    {//being of scope h
      /*...*/
    }//end of scope h
    /*...*/
  }//end of scope g
  /*...*/
}//end of scope f

entonces h podría acceder a todas las variables declaradas en el alcance de la función h, gy f. Esto se hace con cierres. En JavaScript cierres nos permite acceder a cualquier variable declarada en la función parental léxica, en la función de gran abanico lexical, en la función parental léxica granadre, etc. Esto puede verse como un cadena de alcance; scope of current function -> scope of lexical parent function -> scope of lexical grand parent function -> ...  hasta la última función principal que no tiene un padre léxico.

El objeto ventana

En realidad, la cadena no se detiene en la última función principal. Hay un alcance especial más; el alcance global. Cada variable no declarada en una función se considera declarada en el alcance global. El alcance global tiene dos especialidades;

  • cada variable declarada en el alcance global es accesible en todos lados
  • las variables declaradas en el alcance global corresponden a las propiedades del windowobjeto.

Por lo tanto, hay exactamente dos formas de declarar una variable foo en el alcance global; ya sea por no declararlo en una función o estableciendo la propiedad foo del objeto ventana

Ambos intentos usan cierres

Ahora que ha leído una explicación más detallada, ahora puede ser evidente que ambas soluciones usan cierres. Pero para estar seguros, demos una prueba.

Creemos un nuevo Lenguaje de Programación; JavaScript sin cierre. Como su nombre lo sugiere, JavaScript-No-Closure es idéntico a JavaScript, excepto que no admite Closures.

En otras palabras;

var foo = 'hello';
function f(){console.log(foo)};
f();
//JavaScript-No-Closure prints undefined
//JavaSript prints hello

De acuerdo, veamos qué ocurre con la primera solución con JavaScript-No-Closure;

for(var i = 0; i < 10; i++) {
  (function(){
    var i2 = i;
    setTimeout(function(){
        console.log(i2); //i2 is undefined in JavaScript-No-Closure 
    }, 1000)
  })();
}

por lo tanto esto imprimirá undefined 10 veces en JavaScript-No-Closure.

Por lo tanto, la primera solución usa cierre.

Veamos la segunda solución;

for(var i = 0; i < 10; i++) {
  setTimeout((function(i2){
    return function() {
        console.log(i2); //i2 is undefined in JavaScript-No-Closure
    }
  })(i), 1000);
}

por lo tanto esto imprimirá undefined 10 veces en JavaScript-No-Closure.

Ambas soluciones usan cierres.

Editar: se supone que estos 3 fragmentos de código no están definidos en el alcance global. De lo contrario, las variables foo y i se uniría a la window objeto y por lo tanto accesible a través de la window objeto en JavaScript y JavaScript-No-Closure.


46
2017-10-18 01:51



Nunca he estado feliz con la forma en que alguien explica esto.

La clave para comprender los cierres es entender cómo sería JS sin cierres.

Sin cierres, esto arrojaría un error

function outerFunc(){
    var outerVar = 'an outerFunc var';
    return function(){
        alert(outerVar);
    }
}

outerFunc()(); //returns inner function and fires it

Una vez que outerFunc ha regresado en una versión imaginaria desactivada de JavaScript, la referencia a outerVar sería recogida de basura y se habría ido sin dejar nada allí para que la función interna haga referencia.

Los cierres son esencialmente las reglas especiales que se activan y hacen posible que esos vars existan cuando una función interna hace referencia a las variables de una función externa. Con los cierres, los valores a los que se hace referencia se mantienen incluso después de que la función externa finaliza o se "cierra" si eso le ayuda a recordar el punto.

Incluso con cierres, el ciclo de vida de los vars locales en una función sin funciones internas que hacen referencia a sus locales funciona de la misma manera que lo haría en una versión sin cierre. Cuando la función finaliza, los lugareños obtienen basura recolectada.

Una vez que tiene una referencia en un func interno a una var externa, sin embargo, es como si una jamba de puerta se pusiera en el camino de la recolección de basura para los vars referenciados.

Una forma quizás más precisa de mirar los cierres, es que la función interna básicamente utiliza el alcance interno como su propia definición de alcance.

Pero el contexto al que se hace referencia es, de hecho, persistente, no como una instantánea. Disparar repetidamente una función interna devuelta que sigue incrementando y registrando la var local de una función externa seguirá alertando valores más altos.

function outerFunc(){
    var incrementMe = 0;
    return function(){ incrementMe++; console.log(incrementMe); }
}
var inc = outerFunc();
inc(); //logs 1
inc(); //logs 2

19
2017-10-17 09:08



Ambos usan cierres.

Voy con el Definición de Wikipedia aquí:

En informática, un cierre (también cierre o función léxica)   cierre) es una función o referencia a una función junto con un   entorno de referencia: una tabla que almacena una referencia a cada uno de los   variables no locales (también llamadas variables libres) de esa función.   Un cierre, a diferencia de un puntero de función simple, permite que una función acceda   esas variables no locales, incluso cuando se invoca fuera de su inmediato   alcance léxico

El intento de tu amigo usa claramente la variable i, que no es local, tomando su valor y haciendo una copia para almacenar en el local i2.

Tu propio intento pasa i (que en el sitio de llamada está dentro del alcance) a una función anónima como argumento. Esto no es un cierre hasta el momento, pero luego esa función devuelve otra función que hace referencia a la misma i2. Dado que dentro de la función anónima interna i2 no es un local, esto crea un cierre.


16
2017-10-17 09:03



Tú y tu amigo usan cierres:

Un cierre es un tipo especial de objeto que combina dos cosas: una función y el entorno en el que se creó esa función. El entorno consiste en cualquier variable local que estuviera en el ámbito en el momento en que se creó el cierre.

MDN: https://developer.mozilla.org/en-US/docs/JavaScript/Guide/Closures

En la función de código de tu amigo function(){ console.log(i2); } definido dentro del cierre de la función anónima function(){ var i2 = i; ... y puede leer / escribir variable local i2.

En tu función de código function(){ console.log(i2); } definido dentro del cierre de la función function(i2){ return ... y puede leer / escribir valioso local i2 (declarado en este caso como un parámetro).

En ambos casos funciona function(){ console.log(i2); } luego pasó a setTimeout.

Otro equivalente (pero con menos utilización de memoria) es:

function fGenerator(i2){
    return function(){
        console.log(i2);
    }
}
for(var i = 0; i < 10; i++) {
    setTimeout(fGenerator(i), 1000);
}

12
2017-10-17 09:49



Cierre

Un cierre no es una función, y no una expresión. Debe verse como una especie de "instantánea" de las variables utilizadas fuera del scopecope y utilizada dentro de la función. Gramaticalmente, uno debería decir: 'tomar el cierre de las variables'.

Nuevamente, en otras palabras: un cierre es una copia del contexto relevante de las variables de las cuales depende la función.

Una vez más (ingenuo): un cierre tiene acceso a variables que no se pasan como parámetro.

Tenga en cuenta que estos conceptos funcionales dependen en gran medida del lenguaje / entorno de programación que utilice. En JavaScript, el cierre depende del alcance léxico (que es cierto en la mayoría de los c-idiomas).

Por lo tanto, devolver una función es principalmente devolver una función anónima / sin nombre. Cuando las variables de acceso a la función no se pasan como parámetro, y dentro de su alcance (léxico), se ha realizado un cierre.

Entonces, con respecto a tus ejemplos:

// 1
for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i); // closure, only when loop finishes within 1000 ms,
    }, 1000);           // i = 10 for all functions
}
// 2
for(var i = 0; i < 10; i++) {
    (function(){
        var i2 = i; // closure of i (lexical scope: for-loop)
        setTimeout(function(){
            console.log(i2); // closure of i2 (lexical scope:outer function)
        }, 1000)
    })();
}
// 3
for(var i = 0; i < 10; i++) {
    setTimeout((function(i2){
        return function() {
            console.log(i2); // closure of i2 (outer scope)

        }
    })(i), 1000); // param access i (no closure)
}

Todos están usando cierres. No confunda el punto de ejecución con cierres. Si la "instantánea" de los cierres se toma en el momento incorrecto, los valores pueden ser inesperados, pero sin duda se realiza un cierre.


9
2017-10-17 09:20



Veamos ambas formas:

(function(){
    var i2 = i;
    setTimeout(function(){
        console.log(i2);
    }, 1000)
})();

Declara e inmediatamente ejecuta una función anónima que se ejecuta setTimeout() dentro de su propio contexto. El valor actual de i se conserva haciendo una copia en i2 primero; funciona debido a la ejecución inmediata.

setTimeout((function(i2){
    return function() {
        console.log(i2);
    }
})(i), 1000);

Declara un contexto de ejecución para la función interna por la cual el valor actual de i se conserva en i2; este enfoque también usa la ejecución inmediata para preservar el valor.

Importante

Debe mencionarse que la semántica de ejecución NO es la misma entre ambos enfoques; su función interna se pasa a setTimeout()mientras que su función interna llama setTimeout() sí mismo.

Envolviendo ambos códigos dentro de otro setTimeout() no prueba que solo el segundo enfoque usa cierres, simplemente no es lo mismo para empezar.

Conclusión

Ambos métodos usan cierres, por lo que se reduce al gusto personal; el segundo enfoque es más fácil de "mover" o generalizar.


9
2017-12-21 19:10



Escribí esto hace un tiempo para recordarme qué es un cierre y cómo funciona en JS.

Un cierre es una función que, cuando se llama, utiliza el ámbito en el que se declaró, no el ámbito en el que se llamó. En javaScript, todas las funciones se comportan así. Los valores variables en un ámbito persisten siempre que haya una función que aún los señale. La excepción a la regla es 'esto', que se refiere al objeto que la función tiene adentro cuando se llama.

var z = 1;
function x(){
    var z = 2; 
    y(function(){
      alert(z);
    });
}
function y(f){
    var z = 3;
    f();
}
x(); //alerts '2' 

7
2017-10-17 08:58