¿Cómo funcionan los cierres de JavaScript?


7636

¿Cómo explicaría los cierres de JavaScript a alguien que conozca los conceptos en los que consisten (por ejemplo, funciones, variables y similares), pero que no comprende los cierres por sí mismos?

He visto el ejemplo de Scheme dado en Wikipedia, pero desafortunadamente no ayudó.


391
Mi problema con estas y muchas respuestas es que lo abordan desde una perspectiva abstracta y teórica, en lugar de comenzar explicando simplemente por qué los cierres son necesarios en Javascript y las situaciones prácticas en las que los usa. Terminas con un artículo de tl; dr que tienes que leer todo el tiempo, pensando "pero, ¿por qué?". Simplemente comenzaría con: los cierres son una buena manera de lidiar con las siguientes dos realidades de JavaScript: a. el alcance está en el nivel de función, no en el nivel de bloque y, b. Gran parte de lo que haces en la práctica en JavaScript es asíncrono / evento impulsado.
Jeremy Burton

53
@Redsandro Por un lado, hace que el código controlado por eventos sea mucho más fácil de escribir. Podría activar una función cuando se carga la página para determinar detalles sobre el HTML o las características disponibles. Puedo definir y establecer un controlador en esa función y tener toda esa información de contexto disponible cada vez que se llama al controlador sin tener que volver a consultarlo. Resuelva el problema una vez, reutilice en cada página donde se necesita ese controlador con una sobrecarga reducida en la re-invocación del controlador. ¿Alguna vez ha visto que los mismos datos se vuelven a mapear dos veces en un idioma que no los tiene? Los cierres hacen que sea mucho más fácil evitar ese tipo de cosas.
Erik Reppen

1
@ Erik Reppen gracias por la respuesta. En realidad, tenía curiosidad acerca de los beneficios de este closurecódigo difícil de leer en comparación con el Object Literalque se reutiliza y reduce la sobrecarga de todos modos, pero requiere un código de envoltura 100% menor.
Redsandro

66
Para los programadores de Java, la respuesta corta es que es la función equivalente de una clase interna. Una clase interna también tiene un puntero implícito a una instancia de la clase externa, y se usa con el mismo propósito (es decir, crear controladores de eventos).
Boris van Schooten

8
Este ejemplo práctico me pareció muy útil: youtube.com/watch?v=w1s9PgtEoJs
Abhi el

Respuestas:


7360

Un cierre es una combinación de:

  1. Una función, y
  2. Una referencia al alcance externo de esa función (entorno léxico)

Un entorno léxico es parte de cada contexto de ejecución (marco de pila) y es un mapa entre identificadores (es decir, nombres de variables locales) y valores.

Cada función en JavaScript mantiene una referencia a su entorno léxico externo. Esta referencia se utiliza para configurar el contexto de ejecución creado cuando se invoca una función. Esta referencia permite que el código dentro de la función "vea" las variables declaradas fuera de la función, independientemente de cuándo y dónde se llama la función.

Si una función fue llamada por una función, que a su vez fue llamada por otra función, entonces se crea una cadena de referencias a entornos léxicos externos. Esta cadena se llama cadena de alcance.

En el siguiente código, se innerforma un cierre con el entorno léxico del contexto de ejecución creado cuando foose invoca, cerrando sobre variable secret:

function foo() {
  const secret = Math.trunc(Math.random()*100)
  return function inner() {
    console.log(`The secret number is ${secret}.`)
  }
}
const f = foo() // `secret` is not directly accessible from outside `foo`
f() // The only way to retrieve `secret`, is to invoke `f`

En otras palabras: en JavaScript, las funciones llevan una referencia a una "caja de estado" privada, a la que solo ellas (y cualquier otra función declarada dentro del mismo entorno léxico) tienen acceso. Este cuadro de estado es invisible para la persona que llama de la función, y ofrece un excelente mecanismo para ocultar datos y encapsular.

Y recuerde: las funciones en JavaScript se pueden pasar como variables (funciones de primera clase), lo que significa que estos pares de funcionalidad y estado se pueden pasar alrededor de su programa: similar a cómo podría pasar una instancia de una clase en C ++.

Si JavaScript no tuviera cierres, entonces se tendría que pasar más estado entre funciones explícitamente , haciendo que las listas de parámetros sean más largas y el código sea más ruidoso.

Por lo tanto, si desea que una función siempre tenga acceso a un estado privado, puede usar un cierre.

... y con frecuencia nos qué queremos estado asociado con una función. Por ejemplo, en Java o C ++, cuando agrega una variable de instancia privada y un método a una clase, está asociando el estado con la funcionalidad.

En C y en la mayoría de los otros lenguajes comunes, después de que una función regresa, todas las variables locales ya no son accesibles porque el marco de la pila se destruye. En JavaScript, si declara una función dentro de otra función, las variables locales de la función externa pueden permanecer accesibles después de regresar de ella. De esta manera, en el código anterior, secretpermanece disponible para el objeto de función inner, después de que ha sido devuelto foo.

Usos de cierres

Los cierres son útiles siempre que necesite un estado privado asociado con una función. Este es un escenario muy común, y recuerde: JavaScript no tenía una sintaxis de clase hasta 2015, y todavía no tiene una sintaxis de campo privado. Los cierres satisfacen esta necesidad.

Variables de instancia privada

En el siguiente código, la función se toStringcierra sobre los detalles del automóvil.

function Car(manufacturer, model, year, color) {
  return {
    toString() {
      return `${manufacturer} ${model} (${year}, ${color})`
    }
  }
}
const car = new Car('Aston Martin','V8 Vantage','2012','Quantum Silver')
console.log(car.toString())

Programacion Funcional

En el siguiente código, la función se innercierra sobre ambos fny args.

function curry(fn) {
  const args = []
  return function inner(arg) {
    if(args.length === fn.length) return fn(...args)
    args.push(arg)
    return inner
  }
}

function add(a, b) {
  return a + b
}

const curriedAdd = curry(add)
console.log(curriedAdd(2)(3)()) // 5

Programación Orientada a Eventos

En el siguiente código, la función se onClickcierra sobre la variable BACKGROUND_COLOR.

const $ = document.querySelector.bind(document)
const BACKGROUND_COLOR = 'rgba(200,200,242,1)'

function onClick() {
  $('body').style.background = BACKGROUND_COLOR
}

$('button').addEventListener('click', onClick)
<button>Set background color</button>

Modularización

En el siguiente ejemplo, todos los detalles de implementación están ocultos dentro de una expresión de función ejecutada inmediatamente. Las funciones ticky el toStringcierre sobre el estado privado y las funciones que necesitan para completar su trabajo. Los cierres nos han permitido modularizar y encapsular nuestro código.

let namespace = {};

(function foo(n) {
  let numbers = []
  function format(n) {
    return Math.trunc(n)
  }
  function tick() {
    numbers.push(Math.random() * 100)
  }
  function toString() {
    return numbers.map(format)
  }
  n.counter = {
    tick,
    toString
  }
}(namespace))

const counter = namespace.counter
counter.tick()
counter.tick()
console.log(counter.toString())

Ejemplos

Ejemplo 1

Este ejemplo muestra que las variables locales no se copian en el cierre: el cierre mantiene una referencia a las variables originales propios . Es como si el marco de la pila permaneciera vivo en la memoria incluso después de que la función externa salga.

function foo() {
  let x = 42
  let inner  = function() { console.log(x) }
  x = x+1
  return inner
}
var f = foo()
f() // logs 43

Ejemplo 2

En el siguiente código, tres métodos log, incrementy updatetodos cerca en el mismo entorno léxico.

Y cada vez que createObjectse llama, se crea un nuevo contexto de ejecución (marco de pila) y se crea una variable completamente nueva xy un nuevo conjunto de funciones ( logetc.) que se cierran sobre esta nueva variable.

function createObject() {
  let x = 42;
  return {
    log() { console.log(x) },
    increment() { x++ },
    update(value) { x = value }
  }
}

const o = createObject()
o.increment()
o.log() // 43
o.update(5)
o.log() // 5
const p = createObject()
p.log() // 42

Ejemplo 3

Si está utilizando variables declaradas usando var, tenga cuidado de comprender qué variable está cerrando. Las variables declaradas usando varson izadas. Esto es mucho menos problemático en JavaScript moderno debido a la introducción de lety const.

En el siguiente código, cada vez alrededor del ciclo, innerse crea una nueva función , que se cierra i. Pero debido a que var ise iza fuera del bucle, todas estas funciones internas se cierran sobre la misma variable, lo que significa que el valor final de i(3) se imprime tres veces.

function foo() {
  var result = []
  for (var i = 0; i < 3; i++) {
    result.push(function inner() { console.log(i) } )
  }
  return result
}

const result = foo()
// The following will print `3`, three times...
for (var i = 0; i < 3; i++) {
  result[i]() 
}

Puntos finales

  • Cada vez que se declara una función en JavaScript, se crea un cierre.
  • Devolver un functiondesde dentro de otra función es el ejemplo clásico de un cierre, porque el estado dentro de la función externa está implícitamente disponible para la función interna devuelta, incluso después de que la función externa haya completado la ejecución.
  • Cada vez que se usa eval()dentro de una función, se usa un cierre. El texto evalpuede hacer referencia a variables locales de la función, y en modo no estricto incluso puede crear nuevas variables locales mediante el uso eval('var foo = …').
  • Cuando usa new Function(…)(el constructor de funciones ) dentro de una función, no se cierra sobre su entorno léxico: en su lugar, se cierra sobre el contexto global. La nueva función no puede hacer referencia a las variables locales de la función externa.
  • Un cierre en JavaScript es como mantener una referencia ( NO una copia) al alcance en la declaración del punto de función, que a su vez mantiene una referencia a su alcance externo, y así sucesivamente, hasta el objeto global en la parte superior de La cadena de alcance.
  • Se crea un cierre cuando se declara una función; Este cierre se utiliza para configurar el contexto de ejecución cuando se invoca la función.
  • Se crea un nuevo conjunto de variables locales cada vez que se llama a una función.

Enlaces


74
Esto suena bien: "Un cierre en JavaScript es como guardar una copia de todas las variables locales, tal como estaban cuando una función salió". Pero es engañoso por un par de razones. (1) La llamada a la función no tiene que salir para crear un cierre. (2) No es una copia de los valores de las variables locales sino de las propias variables. (3) No dice quién tiene acceso a estas variables.
dlaliberte

27
El ejemplo 5 muestra un "problema" en el que el código no funciona según lo previsto. Pero no muestra cómo solucionarlo. Esta otra respuesta muestra una forma de hacerlo.
Matt

190
Me gusta cómo esta publicación comienza con grandes letras en negrita que dicen "Los cierres no son mágicos" y termina su primer ejemplo con "La magia es que en JavaScript una referencia de función también tiene una referencia secreta al cierre en el que se creó".
Andrew Macheret

66
El Ejemplo # 3 es mezclar cierres con javascripts de elevación. Ahora creo que explicar solo los cierres es bastante difícil sin incorporar el comportamiento de elevación. Esto me ayudó más: Closures are functions that refer to independent (free) variables. In other words, the function defined in the closure 'remembers' the environment in which it was created.de developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
caramba

3
ECMAScript 6 puede cambiar algo en este gran artículo sobre el cierre. Por ejemplo, si usa en let i = 0lugar del var i = 0Ejemplo 5, testList()imprimirá lo que desea originalmente.
Nier

3989

Cada función en JavaScript mantiene un enlace a su entorno léxico externo. Un entorno léxico es un mapa de todos los nombres (por ejemplo, variables, parámetros) dentro de un alcance, con sus valores.

Entonces, cada vez que vea la functionpalabra clave, el código dentro de esa función tiene acceso a las variables declaradas fuera de la función.

function foo(x) {
  var tmp = 3;

  function bar(y) {
    console.log(x + y + (++tmp)); // will log 16
  }

  bar(10);
}

foo(2);

Esto registrará 16porque la función se barcierra sobre el parámetro xy la variable tmp, los cuales existen en el entorno léxico de la función externa foo.

La función bar, junto con su vínculo con el entorno léxico de la función, fooes un cierre.

Una función no tiene que volver para crear un cierre. Simplemente en virtud de su declaración, cada función se cierra sobre su entorno léxico que lo encierra, formando un cierre.

function foo(x) {
  var tmp = 3;

  return function (y) {
    console.log(x + y + (++tmp)); // will also log 16
  }
}

var bar = foo(2);
bar(10); // 16
bar(10); // 17

La función anterior también registrará 16, porque el código interno bartodavía puede referirse a argumentos xy variables tmp, a pesar de que ya no están directamente en el alcance.

Sin embargo, dado tmpque todavía está dando vueltas dentro bardel cierre, está disponible para ser incrementado. Se incrementará cada vez que llame bar.

El ejemplo más simple de un cierre es este:

var a = 10;

function test() {
  console.log(a); // will output 10
  console.log(b); // will output 6
}
var b = 6;
test();

Cuando se invoca una función de JavaScript, ecse crea un nuevo contexto de ejecución . Junto con los argumentos de la función y el objeto de destino, este contexto de ejecución también recibe un enlace al entorno léxico del contexto de ejecución de llamada, lo que significa que las variables declaradas en el entorno léxico externo (en el ejemplo anterior, ambos ay b) están disponibles en ec.

Cada función crea un cierre porque cada función tiene un enlace a su entorno léxico externo.

Tenga en cuenta que las variables en son visibles desde un cierre, no copias.


24
@feeela: Sí, cada función JS crea un cierre. Las variables a las que no se hace referencia probablemente serán elegibles para la recolección de basura en los motores JS modernos, pero no cambia el hecho de que cuando se crea un contexto de ejecución, ese contexto tiene una referencia al contexto de ejecución adjunto y sus variables, y esa función es un objeto que tiene potencial para ser reubicado en un alcance variable diferente, al tiempo que conserva esa referencia original. Ese es el cierre.

@Ali Acabo de descubrir que el jsFiddle que he proporcionado no prueba nada, ya que deletefalla. Sin embargo, el entorno léxico que la función llevará a cabo como [[Alcance]] (y, en última instancia, utilizará como base para su propio entorno léxico cuando se invoque) se determina cuando se ejecuta la instrucción que define la función. Esto significa que la función se está cerrando sobre TODOS los contenidos del ámbito de ejecución, independientemente de los valores a los que realmente se refiere y si escapa del ámbito. Mire las secciones 13.2 y 10 en la especificación
Asad Saeeduddin

8
Esta fue una buena respuesta hasta que trató de explicar tipos primitivos y referencias. Se equivoca completamente y habla de los literales que se copian, lo que realmente no tiene nada que ver con nada.
Ry-

12
Los cierres son la respuesta de JavaScript a la programación orientada a objetos basada en clases. JS no está basado en clases, por lo que uno tenía que encontrar otra forma de implementar algunas cosas que de otro modo no podrían implementarse.
Bartłomiej Zalewski

2
Esta debería ser la respuesta aceptada. La magia nunca ocurre en la función interna. Sucede cuando se asigna la función externa a una variable. Esto crea un nuevo contexto de ejecución para la función interna, por lo que se puede acumular la "variable privada". Por supuesto que puede, ya que la variable a la que se asignó la función externa ha mantenido el contexto. La primera respuesta solo hace que todo sea más complejo sin explicar lo que realmente sucede allí.
Albert Gao

2442

PRÓLOGO: esta respuesta se escribió cuando la pregunta era:

Como dijo el viejo Albert: "Si no puedes explicárselo a un niño de seis años, realmente no lo entiendes tú mismo". Bueno, intenté explicar los cierres de JS a un amigo de 27 años y fracasé por completo.

¿Alguien puede considerar que tengo 6 años y que estoy extrañamente interesado en ese tema?

Estoy bastante seguro de que fui una de las únicas personas que intentó tomar la pregunta inicial literalmente. Desde entonces, la pregunta ha mutado varias veces, por lo que mi respuesta ahora puede parecer increíblemente tonta y fuera de lugar. Esperemos que la idea general de la historia siga siendo divertida para algunos.


Soy un gran admirador de la analogía y la metáfora al explicar conceptos difíciles, así que déjame probar suerte con una historia.

Érase una vez:

Había una princesa ...

function princess() {

Ella vivía en un mundo maravilloso lleno de aventuras. Conoció a su Príncipe Azul, cabalgó alrededor de su mundo en un unicornio, luchó contra dragones, encontró animales que hablaban y muchas otras cosas fantásticas.

    var adventures = [];

    function princeCharming() { /* ... */ }

    var unicorn = { /* ... */ },
        dragons = [ /* ... */ ],
        squirrel = "Hello!";

    /* ... */

Pero ella siempre tendría que regresar a su aburrido mundo de tareas y adultos.

    return {

Y a menudo les contaba su última aventura increíble como princesa.

        story: function() {
            return adventures[adventures.length - 1];
        }
    };
}

Pero todo lo que verían es una niña ...

var littleGirl = princess();

... contando historias sobre magia y fantasía.

littleGirl.story();

Y a pesar de que los adultos sabían de princesas reales, nunca creerían en los unicornios o dragones porque nunca podrían verlos. Los adultos dijeron que solo existían dentro de la imaginación de la niña.

Pero sabemos la verdad real; que la niña con la princesa adentro ...

... es realmente una princesa con una niña dentro.


340
Me encanta esta explicación, de verdad. Para aquellos que lo leen y no siguen, la analogía es la siguiente: la función princess () es un ámbito complejo que contiene datos privados. Fuera de la función, los datos privados no se pueden ver ni acceder. La princesa mantiene los unicornios, dragones, aventuras, etc. en su imaginación (datos privados) y los adultos no pueden verlos por sí mismos. PERO la imaginación de la princesa está capturada en el cierre de la story()función, que es la única interfaz que la littleGirlinstancia expone al mundo de la magia.
Patrick M

Así que aquí storyestá el cierre, pero si el código hubiera sido, var story = function() {}; return story;entonces littleGirlsería el cierre. Al menos esa es la impresión que obtengo del uso de MDN de métodos 'privados' con cierres : "Esas tres funciones públicas son cierres que comparten el mismo entorno".
icc97

16
@ icc97, sí, storyes un cierre que hace referencia al entorno proporcionado dentro del alcance de princess. princessTambién es otro cierre implícito , es decir, el princessy el littleGirlcompartirían cualquier referencia a una parentsmatriz que existiría en el entorno / ámbito donde littleGirlexiste y princessse define el.
Jacob Swartwood

66
@BenjaminKrupp He agregado un comentario de código explícito para mostrar / implicar que hay más operaciones dentro del cuerpo de princesslo que está escrito. Desafortunadamente, esta historia ahora está un poco fuera de lugar en este hilo. Originalmente, la pregunta era "explicar los cierres de JavaScript a 5 años"; mi respuesta fue la única que incluso intentó hacerlo. No dudo que hubiera fallado miserablemente, pero al menos esta respuesta podría haber tenido la oportunidad de mantener el interés de un niño de 5 años.
Jacob Swartwood

11
En realidad, para mí esto tenía mucho sentido. Y debo admitir que, finalmente, entender un cierre de JS usando cuentos de princesas y aventuras me hace sentir un poco raro.
Cristalice el

753

Tomando la pregunta en serio, deberíamos descubrir de qué es capaz cognitivamente un niño típico de 6 años, aunque es cierto que uno que está interesado en JavaScript no es tan típico.

Sobre el desarrollo infantil: 5 a 7 años dice:

Su hijo podrá seguir instrucciones de dos pasos. Por ejemplo, si le dice a su hijo: "Vaya a la cocina y tráigame una bolsa de basura", podrán recordar esa dirección.

Podemos usar este ejemplo para explicar los cierres, de la siguiente manera:

La cocina es un cierre que tiene una variable local, llamada trashBags. Hay una función dentro de la cocina llamada getTrashBagque obtiene una bolsa de basura y la devuelve.

Podemos codificar esto en JavaScript así:

function makeKitchen() {
  var trashBags = ['A', 'B', 'C']; // only 3 at first

  return {
    getTrashBag: function() {
      return trashBags.pop();
    }
  };
}

var kitchen = makeKitchen();

console.log(kitchen.getTrashBag()); // returns trash bag C
console.log(kitchen.getTrashBag()); // returns trash bag B
console.log(kitchen.getTrashBag()); // returns trash bag A

Otros puntos que explican por qué los cierres son interesantes:

  • Cada vez que makeKitchen()se llama, se crea un nuevo cierre con su propio separado trashBags.
  • La trashBagsvariable es local en el interior de cada cocina y no es accesible desde el exterior, pero la función interna de la getTrashBagpropiedad tiene acceso a ella.
  • Cada llamada a la función crea un cierre, pero no sería necesario mantener el cierre a menos que una función interna, que tiene acceso al interior del cierre, pueda llamarse desde el exterior del cierre. Devolver el objeto con la getTrashBagfunción hace eso aquí.

66
En realidad, confusamente, la función makeKitchen llamada es el cierre real, no el objeto de la cocina que devuelve.
dlaliberte

66
Al abrirme paso entre los demás, encontré esta respuesta como la forma más fácil de explicar qué y por qué los cierres.
Chetabahana

3
Demasiado menú y aperitivo, no hay suficiente carne y papas. Podría mejorar esa respuesta con solo una oración corta como: "Un cierre es el contexto sellado de una función, a falta de cualquier mecanismo de alcance proporcionado por las clases".
Staplerfahrer

584

El hombre de paja

Necesito saber cuántas veces se ha hecho clic en un botón y hacer algo cada tres clics ...

Solución bastante obvia

// Declare counter outside event handler's scope
var counter = 0;
var element = document.getElementById('button');

element.addEventListener("click", function() {
  // Increment outside counter
  counter++;

  if (counter === 3) {
    // Do something every third time
    console.log("Third time's the charm!");

    // Reset counter
    counter = 0;
  }
});
<button id="button">Click Me!</button>

Ahora esto funcionará, pero invade el alcance externo al agregar una variable, cuyo único propósito es realizar un seguimiento del recuento. En algunas situaciones, esto sería preferible ya que su aplicación externa podría necesitar acceso a esta información. Pero en este caso, solo estamos cambiando el comportamiento de cada tercer clic, por lo que es preferible incluir esta funcionalidad dentro del controlador de eventos .

Considera esta opción

var element = document.getElementById('button');

element.addEventListener("click", (function() {
  // init the count to 0
  var count = 0;

  return function(e) { // <- This function becomes the click handler
    count++; //    and will retain access to the above `count`

    if (count === 3) {
      // Do something every third time
      console.log("Third time's the charm!");

      //Reset counter
      count = 0;
    }
  };
})());
<button id="button">Click Me!</button>

Observe algunas cosas aquí.

En el ejemplo anterior, estoy usando el comportamiento de cierre de JavaScript. Este comportamiento permite que cualquier función tenga acceso al ámbito en el que se creó, de forma indefinida. Para aplicar esto prácticamente, invoco inmediatamente una función que devuelve otra función, y debido a que la función que estoy devolviendo tiene acceso a la variable de recuento interno (debido al comportamiento de cierre explicado anteriormente), esto da como resultado un ámbito privado para el uso por el resultado función ... ¿No es tan simple? Vamos a diluirlo ...

Un simple cierre de una línea

//          _______________________Immediately invoked______________________
//         |                                                                |
//         |        Scope retained for use      ___Returned as the____      |
//         |       only by returned function   |    value of func     |     |
//         |             |            |        |                      |     |
//         v             v            v        v                      v     v
var func = (function() { var a = 'val'; return function() { alert(a); }; })();

Todas las variables fuera de la función devuelta están disponibles para la función devuelta, pero no están directamente disponibles para el objeto de la función devuelta ...

func();  // Alerts "val"
func.a;  // Undefined

¿Consíguelo? Entonces, en nuestro ejemplo principal, la variable de conteo está contenida dentro del cierre y siempre está disponible para el controlador de eventos, por lo que conserva su estado de clic en clic.

Además, este estado de variable privada es totalmente accesible, tanto para las lecturas como para la asignación a sus variables de ámbito privado.

Ahí tienes ahora está encapsulando completamente este comportamiento.

Publicación de blog completa (incluidas las consideraciones de jQuery)


11
No estoy de acuerdo con tu definición de qué es un cierre. No hay razón para que se invoque a sí mismo. También es un poco simplista (e inexacto) decir que tiene que ser "devuelto" (mucha discusión sobre esto en los comentarios de la respuesta principal a esta pregunta)
James Montagne

40
@James, incluso si no estás de acuerdo, su ejemplo (y toda la publicación) es uno de los mejores que he visto. Si bien la pregunta no es antigua y está resuelta para mí, merece totalmente un +1.
e-satis

84
"Necesito saber cuántas veces se ha hecho clic en un botón y hacer algo cada tres clics ..." Esto me llamó la atención. Un caso de uso y la solución que muestra cómo un cierre no es algo tan misterioso y que muchos de nosotros los hemos estado escribiendo pero no sabíamos exactamente el nombre oficial.
Chris22

Un buen ejemplo porque muestra que "contar" en el segundo ejemplo conserva el valor de "contar" y no se restablece a 0 cada vez que se hace clic en el "elemento". ¡Muy informativo!
Adam

+1 por el comportamiento de cierre . ¿Podemos limitar el comportamiento de cierre a funciones en JavaScript o este concepto también se puede aplicar a otras estructuras del lenguaje?
Dziamid

493

Los cierres son difíciles de explicar porque se utilizan para hacer que funcione un comportamiento que todos intuitivamente esperan que funcione de todos modos. Creo que la mejor manera de explicar ellos (y la forma en que me enteré de lo que hacen) es imaginar la situación sin ellos:

    var bind = function(x) {
        return function(y) { return x + y; };
    }
    
    var plus5 = bind(5);
    console.log(plus5(3));

¿Qué pasaría aquí si JavaScript no supiera los cierres? Simplemente reemplace la llamada en la última línea por su cuerpo de método (que es básicamente lo que hacen las llamadas de función) y obtiene:

console.log(x + 3);

Ahora, ¿dónde está la definición de x? No lo definimos en el alcance actual. La única solución es dejar plus5 llevar su alcance (o más bien, el alcance de su padre). De esta manera, xestá bien definido y está vinculado al valor 5.


11
Este es exactamente el tipo de ejemplo que induce a error a muchas personas a pensar que son los valores los que se utilizan en la función devuelta, no la variable modificable en sí misma. Si se cambiara a "return x + = y", o mejor aún, tanto eso como otra función "x * = y", entonces estaría claro que no se está copiando nada. Para las personas que solían apilar marcos, imagine usar marcos de montón en su lugar, que pueden seguir existiendo después de que la función regrese.
Matt

14
@ Matt no estoy de acuerdo. Un ejemplo es que no supone para documentar exhaustivamente todas las propiedades. Su objetivo es ser reductivo e ilustrar la característica más destacada de un concepto. El OP solicitó una explicación simple ("para un niño de seis años"). Tome la respuesta aceptada: falla por completo en dar una explicación concisa, precisamente porque intenta ser exhaustiva. (Estoy de acuerdo con usted en que es una propiedad importante de JavaScript que el enlace es por referencia en lugar de por valor ... pero, de nuevo, una explicación exitosa es aquella que se reduce al mínimo).
Konrad Rudolph

@KonradRudolph Me gusta el estilo y la brevedad de su ejemplo. Simplemente recomiendo cambiarlo ligeramente para que la parte final, "La única solución es ...", se haga realidad. Actualmente, de hecho, existe otra solución más simple para su escenario, que no corresponde a las continuaciones de JavaScript, y corresponde a un error común sobre qué son las continuaciones. Por lo tanto, el ejemplo en su forma actual es peligroso. Esto no tiene que ver con enumerar exhaustivamente las propiedades, tiene que ver con comprender qué es la x en la función devuelta, que es, después de todo, el punto principal.
Matt

@ Matt Hmm, no estoy seguro de entenderte completamente, pero empiezo a ver que puedes tener un punto válido. Dado que los comentarios son demasiado cortos, ¿podría explicar lo que quiere decir en una idea general o en una sala de chat? Gracias.
Konrad Rudolph

2
@KonradRudolph Creo que no estaba claro sobre el propósito de x + = y. El propósito era simplemente mostrar que las llamadas repetidas a la función devuelta siguen usando la misma variable x (en oposición al mismo valor , que la gente imagina que se "inserta" cuando se crea la función). Esto es como las dos primeras alertas en tu violín. El propósito de una función adicional x * = y sería mostrar que múltiples funciones devueltas comparten la misma x.
Matt

379

TLDR

Un cierre es un enlace entre una función y su entorno léxico externo (es decir, tal como está escrito), de modo que los identificadores (variables, parámetros, declaraciones de funciones, etc.) definidos dentro de ese entorno son visibles desde dentro de la función, independientemente de cuándo o de donde se invoca la función.

Detalles

En la terminología de la especificación ECMAScript, se puede decir que un cierre se implementa mediante la [[Environment]]referencia de cada objeto de función, que apunta al entorno léxico dentro del cual se define la función.

Cuando se invoca una función a través del [[Call]]método interno , la [[Environment]]referencia en el objeto de función se copia en la referencia de entorno externo del registro de entorno del contexto de ejecución recién creado (marco de pila).

En el siguiente ejemplo, la función se fcierra sobre el entorno léxico del contexto de ejecución global:

function f() {}

En el siguiente ejemplo, la función se hcierra sobre el entorno léxico de la función g, que, a su vez, se cierra sobre el entorno léxico del contexto de ejecución global.

function g() {
    function h() {}
}

Si una función interna es devuelta por una externa, entonces el entorno léxico externo persistirá después de que la función externa haya regresado. Esto se debe a que el entorno léxico externo debe estar disponible si finalmente se invoca la función interna.

En el siguiente ejemplo, la función se jcierra sobre el entorno léxico de la función i, lo que significa que la variable xes visible desde la función interna j, mucho después de que la función ihaya completado la ejecución:

function i() {
    var x = 'mochacchino'
    return function j() {
        console.log('Printing the value of x, from within function j: ', x)
    }
} 

const k = i()
setTimeout(k, 500) // invoke k (which is j) after 500ms

En un cierre, las variables en el entorno léxico exterior mismos están disponibles, no copias.

function l() {
  var y = 'vanilla';

  return {
    setY: function(value) {
      y = value;
    },
    logY: function(value) {
      console.log('The value of y is: ', y);
    }
  }
}

const o = l()
o.logY() // The value of y is: vanilla
o.setY('chocolate')
o.logY() // The value of y is: chocolate

La cadena de entornos léxicos, vinculada entre contextos de ejecución a través de referencias de entornos externos, forma una cadena de alcance y define los identificadores visibles desde cualquier función dada.

Tenga en cuenta que, en un intento por mejorar la claridad y la precisión, esta respuesta ha cambiado sustancialmente con respecto a la original.


56
Wow, nunca supe que podría usar sustituciones de cadena de console.logesa manera. Si alguien más está interesado, hay más: developer.mozilla.org/en-US/docs/DOM/…
Flash

77
Las variables que están en la lista de parámetros de la función también son parte del cierre (por ejemplo, no solo limitado a var).
Thomas Eding

Los cierres suenan más como objetos y clases, etc. No estoy seguro de por qué mucha gente no compara estos dos, ¡sería más fácil para nosotros los novatos aprender!
almaruf

376

OK, ventilador de cierres de 6 años. ¿Quieres escuchar el ejemplo más simple de cierre?

Imaginemos la siguiente situación: un conductor está sentado en un automóvil. Ese auto está dentro de un avión. El avión está en el aeropuerto. La capacidad del conductor para acceder a cosas fuera de su automóvil, pero dentro del avión, incluso si ese avión sale de un aeropuerto, es un cierre. Eso es. Cuando cumpla 27 años, mire la explicación más detallada o el ejemplo a continuación.

Aquí es cómo puedo convertir mi historia de avión en el código.

var plane = function(defaultAirport) {

  var lastAirportLeft = defaultAirport;

  var car = {
    driver: {
      startAccessPlaneInfo: function() {
        setInterval(function() {
          console.log("Last airport was " + lastAirportLeft);
        }, 2000);
      }
    }
  };
  car.driver.startAccessPlaneInfo();

  return {
    leaveTheAirport: function(airPortName) {
      lastAirportLeft = airPortName;
    }
  }
}("Boryspil International Airport");

plane.leaveTheAirport("John F. Kennedy");


26
Bien jugado y responde al cartel original. Creo que esta es la mejor respuesta. Iba a usar el equipaje de una manera similar: imagínese que va a la casa de la abuela y empaca su caja de Nintendo DS con tarjetas de juego dentro de su caja, pero luego empaca la caja dentro de su mochila y también pone tarjetas de juego en los bolsillos de su mochila, y ENTONCES pones todo en una maleta grande con más tarjetas de juego en los bolsillos de la maleta. Cuando llegues a la casa de la abuela, puedes jugar cualquier juego en tu DS siempre que todos los casos exteriores estén abiertos. O algo por el estilo.
slartibartfast

366

Este es un intento de aclarar varios (posibles) malentendidos sobre los cierres que aparecen en algunas de las otras respuestas.

  • Un cierre no solo se crea cuando devuelve una función interna. De hecho, la función de cierre no necesita regresar para poder crear su cierre. En su lugar, puede asignar su función interna a una variable en un ámbito externo, o pasarla como un argumento a otra función donde pueda llamarse inmediatamente o en cualquier momento posterior. Por lo tanto, el cierre de la función de cierre probablemente se crea tan pronto como se llama a la función de cierre ya que cualquier función interna tiene acceso a ese cierre cada vez que se llama a la función interna, antes o después de que regrese la función de cierre.
  • Un cierre no hace referencia a una copia de los valores antiguos de las variables en su alcance. Las variables en sí mismas son parte del cierre, por lo que el valor visto al acceder a una de esas variables es el último valor en el momento en que se accede. Es por eso que las funciones internas creadas dentro de los bucles pueden ser complicadas, ya que cada una tiene acceso a las mismas variables externas en lugar de tomar una copia de las variables en el momento en que se crea o se llama a la función.
  • Las "variables" en un cierre incluyen cualquier función nombrada declarada dentro de la función. También incluyen argumentos de la función. Un cierre también tiene acceso a las variables de cierre que lo contienen, hasta el alcance global.
  • Los cierres usan memoria, pero no causan pérdidas de memoria ya que JavaScript por sí solo limpia sus propias estructuras circulares a las que no se hace referencia. Las pérdidas de memoria de Internet Explorer que involucran cierres se crean cuando no se desconectan los valores de los atributos DOM que hacen referencia a los cierres, manteniendo así referencias a estructuras posiblemente circulares.

15
James, dije que el cierre es "probablemente" creado en el momento de la llamada de la función de cierre porque es plausible que una implementación pueda diferir la creación de un cierre hasta algún momento más tarde, cuando decida que un cierre es absolutamente necesario. Si no hay una función interna definida en la función de cierre, entonces no se necesitará ningún cierre. Entonces, tal vez podría esperar hasta que se cree la primera función interna para luego crear un cierre fuera del contexto de llamada de la función de cierre.
dlaliberte

99
@ Remolacha-Remolacha Supongamos que tenemos una función interna que se pasa a otra función donde se usa antes de que regrese la función externa, y supongamos que también devolvemos la misma función interna de la función externa. Es idénticamente la misma función en ambos casos, pero está diciendo que antes de que regrese la función externa, la función interna está "vinculada" a la pila de llamadas, mientras que después de que regrese, la función interna está repentinamente vinculada a un cierre. Se comporta de manera idéntica en ambos casos; la semántica es idéntica, entonces, ¿no estás hablando solo de detalles de implementación?
dlaliberte

77
@ Remolacha-Remolacha, gracias por tus comentarios, y me alegra haberte hecho pensar. Todavía no veo ninguna diferencia semántica entre el contexto en vivo de la función externa y ese mismo contexto cuando se convierte en un cierre cuando la función regresa (si entiendo su definición). A la función interna no le importa. A la recolección de basura no le importa, ya que la función interna mantiene una referencia al contexto / cierre de cualquier manera, y el llamador de la función externa simplemente descarta su referencia al contexto de la llamada. Pero es confuso para las personas y quizás sea mejor llamarlo contexto de llamada.
dlaliberte

99
Ese artículo es difícil de leer, pero creo que en realidad respalda lo que digo. Dice: "Un cierre se forma al devolver un objeto de función [...] o al asignar directamente una referencia a dicho objeto de función, por ejemplo, a una variable global". No quiero decir que GC sea irrelevante. Más bien, debido a GC, y debido a que la función interna está unida al contexto de llamada de la función externa (o [[alcance]] como dice el artículo), entonces no importa si la llamada a la función externa regresa porque ese enlace con el interno La función es lo importante.
dlaliberte

3
¡Gran respuesta! Una cosa que debe agregar es que todas las funciones se cierran sobre todo el contenido del ámbito de ejecución en el que están definidas. No importa si se refieren a alguna o ninguna de las variables del ámbito primario: una referencia al entorno léxico del ámbito primario se almacena como [[Alcance]] incondicionalmente. Esto se puede ver en la sección sobre creación de funciones en la especificación ECMA.
Asad Saeeduddin

236

Escribí una publicación de blog hace un tiempo explicando los cierres. Esto es lo que dije sobre los cierres en términos de por qué querrías uno.

Los cierres son una forma de permitir que una función tenga variables privadas persistentes, es decir, variables que solo una función conoce, donde puede realizar un seguimiento de la información de tiempos anteriores en que se ejecutó.

En ese sentido, dejan que una función actúe un poco como un objeto con atributos privados.

Publicación completa:

Entonces, ¿qué son estas cosas de cierre?


Entonces, ¿se podría enfatizar el beneficio principal de los cierres con este ejemplo? Digamos que tengo una función emailError (sendToAddress, errorString) que podría decir devError = emailError("devinrhode2@googmail.com", errorString)y luego tener mi propia versión personalizada de una función compartida de emailError?
Devin G Rhode

¡Esta explicación y el ejemplo perfecto asociado en el enlace a (cosas de cierre) es la mejor manera de entender los cierres y debe estar justo en la parte superior!
HopeKing

215

Los cierres son simples:

El siguiente ejemplo simple cubre todos los puntos principales de los cierres de JavaScript. * *  

Aquí hay una fábrica que produce calculadoras que pueden sumar y multiplicar:

function make_calculator() {
  var n = 0; // this calculator stores a single number n
  return {
    add: function(a) {
      n += a;
      return n;
    },
    multiply: function(a) {
      n *= a;
      return n;
    }
  };
}

first_calculator = make_calculator();
second_calculator = make_calculator();

first_calculator.add(3); // returns 3
second_calculator.add(400); // returns 400

first_calculator.multiply(11); // returns 33
second_calculator.multiply(10); // returns 4000

El punto clave: cada llamada a make_calculatorcrea una nueva variable local n, que sigue siendo utilizable por esa calculadora addy multiplyfunciona mucho después de los make_calculatorretornos.

Si está familiarizado con los marcos de pila, estas calculadoras parecen extrañas: ¿cómo pueden seguir accediendo ndespués de las make_calculatordevoluciones? La respuesta es imaginar que JavaScript no usa "marcos de pila", sino que usa "marcos de montón", que pueden persistir después de la llamada a la función que los hizo regresar.

Las funciones internas como addy multiply, que variables de acceso declaradas en una función externa ** , se denominan cierres .

Eso es casi todo lo que hay para los cierres.



* Por ejemplo, cubre todos los puntos en el artículo "Cierres para tontos" que se da en otra respuesta , excepto el ejemplo 6, que simplemente muestra que las variables se pueden usar antes de que se declaren, un hecho agradable de conocer pero completamente ajeno a los cierres. También cubre todos los puntos en la respuesta aceptada , excepto los puntos (1) que las funciones copian sus argumentos en variables locales (los argumentos de la función nombrada), y (2) que copiar números crea un nuevo número, pero copiar una referencia de objeto le da otra referencia al mismo objeto. También es bueno saberlo, pero nuevamente no tiene nada que ver con los cierres. También es muy similar al ejemplo en esta respuesta, pero un poco más corto y menos abstracto. No cubre el punto deesta respuesta o este comentario , que es que JavaScript dificulta la conexión actualvalor de una variable de bucle en su función interna: el paso de "conexión" solo se puede hacer con una función auxiliar que encierra su función interna y se invoca en cada iteración del bucle. (Estrictamente hablando, la función interna accede a la copia de la variable de la función auxiliar, en lugar de tener algo enchufado). De nuevo, es muy útil cuando se crean cierres, pero no es parte de qué es un cierre o cómo funciona. Existe una confusión adicional debido a que los cierres funcionan de manera diferente en lenguajes funcionales como ML, donde las variables están vinculadas a valores en lugar de espacio de almacenamiento, proporcionando un flujo constante de personas que entienden los cierres de una manera (es decir, la forma de "enchufar") que es simplemente incorrecto para JavaScript, donde las variables siempre están vinculadas al espacio de almacenamiento y nunca a los valores.

** Cualquier función externa, si varias están anidadas, o incluso en el contexto global, como esta respuesta señala claramente.


¿Qué pasaría si llamaras: second_calculator = first_calculator (); en lugar de second_calculator = make_calculator (); ? Debería ser lo mismo, ¿verdad?
Ronen Festinger

44
@Ronen: Dado que first_calculatores un objeto (no una función) no debe usar paréntesis second_calculator = first_calculator;, ya que es una asignación, no una llamada a la función. Para responder a su pregunta, solo habría una llamada a make_calculator, por lo que solo se haría una calculadora, y las variables first_calculator y second_calculator se referirían a la misma calculadora, por lo que las respuestas serían 3, 403, 4433, 44330.
Matt

204

Cómo se lo explicaría a un niño de seis años:

¿Sabes cómo los adultos pueden ser dueños de una casa y la llaman hogar? Cuando una madre tiene un hijo, el niño realmente no posee nada, ¿verdad? Pero sus padres son dueños de una casa, por lo que cada vez que alguien le pregunta al niño "¿Dónde está tu casa?", Él / ella puede responder "¡esa casa!" Y señalar la casa de sus padres. Un "cierre" es la capacidad del niño de siempre (incluso si está en el extranjero) poder decir que tiene un hogar, aunque en realidad son los padres quienes poseen la casa.


200

¿Puedes explicar los cierres a un niño de 5 años? *

Todavía creo que la explicación de Google funciona muy bien y es conciso:

/*
*    When a function is defined in another function and it
*    has access to the outer function's context even after
*    the outer function returns.
*
* An important concept to learn in JavaScript.
*/

function outerFunction(someNum) {
    var someString = 'Hey!';
    var content = document.getElementById('content');
    function innerFunction() {
        content.innerHTML = someNum + ': ' + someString;
        content = null; // Internet Explorer memory leak for DOM reference
    }
    innerFunction();
}

outerFunction(1);​

Prueba de que este ejemplo crea un cierre incluso si la función interna no regresa

* AC # pregunta


11
El código es "correcto", como un ejemplo de un cierre, a pesar de que no aborda la parte del comentario sobre el uso del cierre después de que la función exterior vuelve. Entonces no es un gran ejemplo. Hay muchas otras formas en que se podría usar un cierre que no implique devolver la función inner. por ejemplo, innerFunction podría pasarse a otra función donde se llama de inmediato o se almacena y se llama algún tiempo después, y en todos los casos, tiene acceso al contexto de la función externa que se creó cuando se llamó.
dlaliberte

66
@syockit No, Moss está equivocado. Se crea un cierre independientemente de si la función alguna vez escapa del alcance en el que se define, y una referencia creada incondicionalmente al entorno léxico primario hace que todas las variables en el alcance primario estén disponibles para todas las funciones, independientemente de si se invocan fuera o dentro El alcance en el que fueron creados.
Asad Saeeduddin

176

Tiendo a aprender mejor mediante comparaciones BUENAS / MALAS. Me gusta ver el código de trabajo seguido de un código que no funciona que es probable que alguien encuentre. Monté un jsFiddle que hace una comparación y trata de reducirse las diferencias a las explicaciones más simples que pudiera ocurrir.

Cierres bien hechos:

console.log('CLOSURES DONE RIGHT');

var arr = [];

function createClosure(n) {
    return function () {
        return 'n = ' + n;
    }
}

for (var index = 0; index < 10; index++) {
    arr[index] = createClosure(index);
}

for (var index in arr) {
    console.log(arr[index]());
}
  • En el código anterior createClosure(n)se invoca en cada iteración del bucle. Tenga en cuenta que nombré la variable npara resaltar que es una nueva variable creada en un nuevo alcance de función y no es la misma variable indexque está vinculada al alcance externo.

  • Esto crea un nuevo alcance y nestá vinculado a ese alcance; Esto significa que tenemos 10 ámbitos separados, uno para cada iteración.

  • createClosure(n) devuelve una función que devuelve la n dentro de ese alcance.

  • Dentro de cada ámbito nestá vinculado al valor que tenía cuando createClosure(n)se invocó, por lo que la función anidada que se devuelve siempre devolverá el valor nque tenía cuando createClosure(n)se invocó.

Cierres mal hechos:

console.log('CLOSURES DONE WRONG');

function createClosureArray() {
    var badArr = [];

    for (var index = 0; index < 10; index++) {
        badArr[index] = function () {
            return 'n = ' + index;
        };
    }
    return badArr;
}

var badArr = createClosureArray();

for (var index in badArr) {
    console.log(badArr[index]());
}
  • En el código anterior, el bucle se movió dentro de la createClosureArray()función y la función ahora solo devuelve la matriz completa, que a primera vista parece más intuitiva.

  • Lo que podría no ser obvio es que, dado que createClosureArray()solo se invoca una vez que se crea un solo alcance para esta función en lugar de uno para cada iteración del bucle.

  • Dentro de esta función indexse define una variable llamada . El ciclo se ejecuta y agrega funciones a la matriz que regresan index. Tenga en cuenta que indexse define dentro de la createClosureArrayfunción que solo se invoca una vez.

  • Debido a que solo había un alcance dentro de la createClosureArray()función, indexsolo está vinculado a un valor dentro de ese alcance. En otras palabras, cada vez que el ciclo cambia el valor de index, lo cambia por todo lo que hace referencia dentro de ese alcance.

  • Todas las funciones agregadas a la matriz devuelven la MISMA indexvariable del ámbito primario donde se definió en lugar de 10 diferentes de 10 ámbitos diferentes como el primer ejemplo. El resultado final es que las 10 funciones devuelven la misma variable del mismo ámbito.

  • Después de que el ciclo terminó y indexse modificó, el valor final fue 10, por lo tanto, cada función agregada a la matriz devuelve el valor de la indexvariable única que ahora se establece en 10.

Resultado

CIERRES HECHOS A LA DERECHA
n = 0
n = 1
n = 2
n = 3
n = 4
n = 5
n = 6
n = 7
n = 8
n = 9

CIERRES HECHOS INCORRECTOS
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10


1
Buena adición, gracias. Solo para que quede más claro, uno puede imaginar cómo se crea la matriz "mala" en el bucle "malo" con cada iteración: 1ra iteración: [function () {return 'n =' + 0;}] 2da iteración: [( function () {return 'n =' + 1;}), (function () {return 'n =' + 1;})] 3ra iteración: [(function () {return 'n =' + 2;}) , (function () {return 'n =' + 2;}), (function () {return 'n =' + 2;})] etc. Entonces, cada vez que cambia el valor del índice se refleja en todas las funciones ya agregado a la matriz.
Alex Alexeev

3
Usando letpara vararregla la diferencia.
Rupam Datta

¿No es aquí "El cierre bien hecho" es un ejemplo de "cierre dentro del cierre"?
TechnicalSmile

Quiero decir, cada función es técnicamente un cierre, pero la parte importante es que la función define una nueva variable dentro. La función que obtiene devuelve solo referencias ncreadas en un nuevo cierre. Simplemente devolvemos una función para poder almacenarla en la matriz e invocarla más tarde.
Chev

Si sólo quiere guardar el resultado en la matriz en la primera iteración, entonces podría inline así: arr[index] = (function (n) { return 'n = ' + n; })(index);. Pero luego está almacenando la cadena resultante en la matriz en lugar de una función para invocar que derrota el punto de mi ejemplo.
Chev

164

Wikipedia sobre cierres :

En informática, un cierre es una función junto con un entorno de referencia para los nombres no locales (variables libres) de esa función.

Técnicamente, en JavaScript , cada función es un cierre . Siempre tiene acceso a variables definidas en el ámbito circundante.

Dado que la construcción que define el alcance en JavaScript es una función , no un bloque de código como en muchos otros lenguajes, lo que generalmente queremos decir con cierre en JavaScript es una función que trabaja con variables no locales definidas en una función circundante ya ejecutada .

Los cierres se usan a menudo para crear funciones con algunos datos privados ocultos (pero no siempre es así).

var db = (function() {
    // Create a hidden object, which will hold the data
    // it's inaccessible from the outside.
    var data = {};

    // Make a function, which will provide some access to the data.
    return function(key, val) {
        if (val === undefined) { return data[key] } // Get
        else { return data[key] = val } // Set
    }
    // We are calling the anonymous surrounding function,
    // returning the above inner function, which is a closure.
})();

db('x')    // -> undefined
db('x', 1) // Set x to 1
db('x')    // -> 1
// It's impossible to access the data object itself.
// We are able to get or set individual it.

ems

El ejemplo anterior está utilizando una función anónima, que se ejecutó una vez. Pero no tiene que ser así. Puede nombrarse (p mkdb. Ej. ) Y ejecutarse más tarde, generando una función de base de datos cada vez que se invoca. Cada función generada tendrá su propio objeto de base de datos oculto. Otro ejemplo de uso de los cierres es cuando no devolvemos una función, sino un objeto que contiene múltiples funciones para diferentes propósitos, cada una de esas funciones tiene acceso a los mismos datos.


2
Esta es la mejor explicación para los cierres de JavaScript. Debería ser la respuesta elegida. El resto es lo suficientemente entretenido, pero este es realmente útil de manera práctica para los codificadores de JavaScript del mundo real.
geodésico

136

Creé un tutorial interactivo de JavaScript para explicar cómo funcionan los cierres. ¿Qué es un cierre?

Aquí hay uno de los ejemplos:

var create = function (x) {
    var f = function () {
        return x; // We can refer to x here!
    };
    return f;
};
// 'create' takes one argument, creates a function

var g = create(42);
// g is a function that takes no arguments now

var y = g();
// y is 42 here

128

Los niños siempre recordarán los secretos que han compartido con sus padres, incluso después de que sus padres se hayan ido. Esto es lo que son los cierres para las funciones.

Los secretos para las funciones de JavaScript son las variables privadas.

var parent = function() {
 var name = "Mary"; // secret
}

Cada vez que lo llama, se crea la variable local "nombre" y se le da el nombre "María". Y cada vez que sale la función, se pierde la variable y se olvida el nombre.

Como puede suponer, debido a que las variables se vuelven a crear cada vez que se llama a la función, y nadie más las conocerá, debe haber un lugar secreto donde se almacenan. Podría llamarse Cámara de los Secretos o pila o ámbito local, pero en realidad no importa. Sabemos que están allí, en algún lugar, escondidos en la memoria.

Pero, en JavaScript, existe esta cosa muy especial que las funciones que se crean dentro de otras funciones, también pueden conocer las variables locales de sus padres y mantenerlas mientras vivan.

var parent = function() {
  var name = "Mary";
  var child = function(childName) {
    // I can also see that "name" is "Mary"
  }
}

Entonces, mientras estemos en la función principal, puede crear una o más funciones secundarias que comparten las variables secretas del lugar secreto.

Pero lo triste es que si el niño también es una variable privada de su función principal, también moriría cuando el padre terminara, y los secretos morirían con ellos.

Entonces, para vivir, el niño tiene que irse antes de que sea demasiado tarde

var parent = function() {
  var name = "Mary";
  var child = function(childName) {
    return "My name is " + childName  +", child of " + name; 
  }
  return child; // child leaves the parent ->
}
var child = parent(); // < - and here it is outside 

Y ahora, a pesar de que Mary "ya no está corriendo", el recuerdo de ella no se pierde y su hijo siempre recordará su nombre y otros secretos que compartieron durante su tiempo juntos.

Entonces, si llamas a la niña "Alice", ella responderá

child("Alice") => "My name is Alice, child of Mary"

Eso es todo lo que hay que contar.


15
Esta es la explicación que tiene más sentido para mí porque no supone un conocimiento previo significativo de términos técnicos. La explicación más votada aquí supone que la persona que no entiende los cierres tiene una comprensión completa y completa de términos como 'alcance léxico' y 'contexto de ejecución', aunque puedo entender esto conceptualmente, no creo que sea tan cómodo con los detalles de ellos como debería estar, y la explicación sin jerga en absoluto es lo que hizo que los cierres finalmente hagan clic para mí, gracias. Como beneficio adicional, creo que también explica qué alcance es muy conciso.
Emma W

103

No entiendo por qué las respuestas son tan complejas aquí.

Aquí hay un cierre:

var a = 42;

function b() { return a; }

Si. Probablemente lo uses muchas veces al día.


No hay razón para creer que los cierres son un truco de diseño complejo para abordar problemas específicos. No, los cierres consisten simplemente en utilizar una variable que proviene de un ámbito más alto desde la perspectiva del lugar donde se declaró la función (no se ejecutó) .

Ahora lo que te permite hacer puede ser más espectacular, mira otras respuestas.


55
No parece probable que esta respuesta ayude a confundir a las personas. Un equivalente aproximado en un lenguaje de programación tradicional podría ser crear b () como método en un objeto que también tiene una constante o propiedad privada a. En mi opinión, la sorpresa es que el objeto de alcance JS efectivamente proporciona acomo una propiedad en lugar de una constante. Y solo notará ese comportamiento importante si lo modifica, como enreturn a++;
Jon Coombs

1
Exactamente lo que dijo Jon. Antes de que finalmente cerrara los cierres, me costaba encontrar ejemplos prácticos. Sí, floribon creó un cierre, pero para mí sin educación esto no me habría enseñado absolutamente nada.
Chev

3
Esto no define qué es un cierre, es simplemente un ejemplo que lo utiliza. Y no aborda el matiz de lo que sucede cuando termina el alcance; No creo que nadie tuviera una pregunta sobre el alcance léxico cuando todavía existen todos los ámbitos, y especialmente en el caso de una variable global.
Gerard ONeill

92

Ejemplo para el primer punto de dlaliberte:

Un cierre no solo se crea cuando devuelve una función interna. De hecho, la función de cierre no necesita regresar en absoluto. En su lugar, puede asignar su función interna a una variable en un ámbito externo, o pasarla como argumento a otra función donde podría usarse de inmediato. Por lo tanto, el cierre de la función de cierre probablemente ya exista en el momento en que se llamó a la función de cierre ya que cualquier función interna tiene acceso a ella tan pronto como se llama.

var i;
function foo(x) {
    var tmp = 3;
    i = function (y) {
        console.log(x + y + (++tmp));
    }
}
foo(2);
i(3);

Pequeña aclaración sobre una posible ambigüedad. Cuando dije "De hecho, la función de cierre no necesita regresar en absoluto". No quise decir "no devolver ningún valor" sino "aún activo". Entonces, el ejemplo no muestra ese aspecto, aunque muestra otra forma en que la función interna se puede pasar al alcance externo. El punto principal que estaba tratando de hacer es sobre el momento de la creación del cierre (para la función de cierre), ya que algunas personas parecen pensar que sucede cuando la función de cierre regresa. Se requiere un ejemplo diferente para mostrar que el cierre se crea cuando se llama a una función .
dlaliberte

89

Un cierre es donde una función interna tiene acceso a variables en su función externa. Esa es probablemente la explicación más simple de una línea que puede obtener para los cierres.


35
Esa es solo la mitad de la explicación. Lo importante a tener en cuenta sobre los cierres es que si todavía se hace referencia a la función interna después de que la función externa haya salido, los valores antiguos de la función externa aún están disponibles para la interna.
pcorcoran

22
En realidad, no son los valores antiguos de la función externa los que están disponibles para la función interna, sino las variables antiguas , que podrían tener valores nuevos si alguna función pudiera cambiarlas.
dlaliberte

86

Sé que ya hay muchas soluciones, pero supongo que este script pequeño y simple puede ser útil para demostrar el concepto:

// makeSequencer will return a "sequencer" function
var makeSequencer = function() {
    var _count = 0; // not accessible outside this function
    var sequencer = function () {
        return _count++;
    }
    return sequencer;
}

var fnext = makeSequencer();
var v0 = fnext();     // v0 = 0;
var v1 = fnext();     // v1 = 1;
var vz = fnext._count // vz = undefined

82

Estás durmiendo e invitas a Dan. Le dices a Dan que traiga un controlador XBox.

Dan invita a Paul. Dan le pide a Paul que traiga un controlador. ¿Cuántos controladores fueron llevados a la fiesta?

function sleepOver(howManyControllersToBring) {

    var numberOfDansControllers = howManyControllersToBring;

    return function danInvitedPaul(numberOfPaulsControllers) {
        var totalControllers = numberOfDansControllers + numberOfPaulsControllers;
        return totalControllers;
    }
}

var howManyControllersToBring = 1;

var inviteDan = sleepOver(howManyControllersToBring);

// The only reason Paul was invited is because Dan was invited. 
// So we set Paul's invitation = Dan's invitation.

var danInvitedPaul = inviteDan(howManyControllersToBring);

alert("There were " + danInvitedPaul + " controllers brought to the party.");

80

El autor de Closures ha explicado los cierres bastante bien, explicando la razón por la que los necesitamos y también explicando el entorno Lexical que es necesario para comprender los cierres.
Aquí está el resumen:

¿Qué pasa si se accede a una variable, pero no es local? Como aquí:

Ingrese la descripción de la imagen aquí

En este caso, el intérprete encuentra la variable en el LexicalEnvironmentobjeto externo .

El proceso consta de dos pasos:

  1. Primero, cuando se crea una función f, no se crea en un espacio vacío. Hay un objeto LexicalEnvironment actual. En el caso anterior, es una ventana (a no está definida en el momento de la creación de la función).

Ingrese la descripción de la imagen aquí

Cuando se crea una función, obtiene una propiedad oculta, llamada [[Scope]], que hace referencia al entorno Lexical actual.

Ingrese la descripción de la imagen aquí

Si se lee una variable, pero no se puede encontrar en ningún lado, se genera un error.

Funciones anidadas

Las funciones se pueden anidar una dentro de otra, formando una cadena de entornos Lexical que también se puede llamar una cadena de alcance.

Ingrese la descripción de la imagen aquí

Entonces, la función g tiene acceso a g, a y f.

Cierres

Una función anidada puede continuar viva después de que la función externa haya finalizado:

Ingrese la descripción de la imagen aquí

Marcado de entornos léxicos:

Ingrese la descripción de la imagen aquí

Como vemos, this.sayes una propiedad en el objeto de usuario, por lo que continúa viva después de que el Usuario haya completado.

Y si recuerda, cuando this.sayse crea, (como cada función) obtiene una referencia interna this.say.[[Scope]]al entorno Lexical actual. Por lo tanto, el entorno Lexical de la ejecución actual del usuario permanece en la memoria. Todas las variables de Usuario también son sus propiedades, por lo que también se guardan cuidadosamente, no se desechan como de costumbre.

El objetivo es asegurarse de que si la función interna quiere acceder a una variable externa en el futuro, pueda hacerlo.

Para resumir:

  1. La función interna mantiene una referencia al entorno Lexical externo.
  2. La función interna puede acceder a las variables en cualquier momento, incluso si la función externa está terminada.
  3. El navegador mantiene el entorno Lexical y todas sus propiedades (variables) en la memoria hasta que haya una función interna que lo haga referencia.

Esto se llama cierre.


78

Las funciones de JavaScript pueden acceder a sus:

  1. Argumentos
  2. Locales (es decir, sus variables locales y funciones locales)
  3. Medio ambiente, que incluye:
    • globales, incluido el DOM
    • cualquier cosa en funciones externas

Si una función accede a su entorno, entonces la función es un cierre.

Tenga en cuenta que las funciones externas no son necesarias, aunque ofrecen beneficios que no discuto aquí. Al acceder a los datos en su entorno, un cierre mantiene esos datos vivos. En el caso de las funciones externas / internas, una función externa puede crear datos locales y eventualmente salir, y sin embargo, si alguna función interna sobrevive después de que la función externa sale, entonces la función interna mantiene los datos locales de la función externa. viva.

Ejemplo de un cierre que utiliza el entorno global:

Imagínese que los eventos de botón Stack Overflow Vote-Up y Vote-Down se implementan como cierres, voteUp_click y voteDown_click, que tienen acceso a variables externas isVotedUp y isVotedDown, que se definen globalmente. (En aras de la simplicidad, me refiero a los botones de votación de preguntas de StackOverflow, no a la matriz de botones de votación de respuestas).

Cuando el usuario hace clic en el botón VoteUp, la función voteUp_click verifica si isVotedDown == true para determinar si votar o simplemente cancelar un voto negativo. La función voteUp_click es un cierre porque está accediendo a su entorno.

var isVotedUp = false;
var isVotedDown = false;

function voteUp_click() {
  if (isVotedUp)
    return;
  else if (isVotedDown)
    SetDownVote(false);
  else
    SetUpVote(true);
}

function voteDown_click() {
  if (isVotedDown)
    return;
  else if (isVotedUp)
    SetUpVote(false);
  else
    SetDownVote(true);
}

function SetUpVote(status) {
  isVotedUp = status;
  // Do some CSS stuff to Vote-Up button
}

function SetDownVote(status) {
  isVotedDown = status;
  // Do some CSS stuff to Vote-Down button
}

Las cuatro funciones son cierres, ya que todos acceden a su entorno.


59

Como padre de un niño de 6 años, que actualmente enseña a niños pequeños (y un novato relativo a la codificación sin educación formal, por lo que se requerirán correcciones), creo que la lección se mantendrá mejor a través del juego práctico. Si el niño de 6 años está listo para entender qué es un cierre, entonces tiene la edad suficiente para intentarlo. Sugeriría pegar el código en jsfiddle.net, explicar un poco y dejarlos solos para inventar una canción única. El texto explicativo a continuación es probablemente más apropiado para un niño de 10 años.

function sing(person) {

    var firstPart = "There was " + person + " who swallowed ";

    var fly = function() {
        var creature = "a fly";
        var result = "Perhaps she'll die";
        alert(firstPart + creature + "\n" + result);
    };

    var spider = function() {
        var creature = "a spider";
        var result = "that wiggled and jiggled and tickled inside her";
        alert(firstPart + creature + "\n" + result);
    };

    var bird = function() {
        var creature = "a bird";
        var result = "How absurd!";
        alert(firstPart + creature + "\n" + result);
    };

    var cat = function() {
        var creature = "a cat";
        var result = "Imagine That!";
        alert(firstPart + creature + "\n" + result);
    };

    fly();
    spider();
    bird();
    cat();
}

var person="an old lady";

sing(person);

INSTRUCCIONES

DATOS: Los datos son una colección de hechos. Pueden ser números, palabras, medidas, observaciones o incluso solo descripciones de cosas. No puedes tocarlo, olerlo o probarlo. Puedes escribirlo, hablarlo y escucharlo. Puede usarlo para crear olores y sabores táctiles usando una computadora. Puede ser útil por una computadora usando código.

CÓDIGO: Toda la escritura anterior se llama código . Está escrito en JavaScript.

JAVASCRIPT: JavaScript es un lenguaje. Al igual que el inglés o francés o chino son idiomas. Hay muchos idiomas que las computadoras y otros procesadores electrónicos entienden. Para que una computadora entienda JavaScript necesita un intérprete. Imagínese si un maestro que solo habla ruso viene a enseñar su clase en la escuela. Cuando el maestro dice "все садятся", la clase no lo entendería. Pero afortunadamente tienes un alumno ruso en tu clase que le dice a todos que esto significa "todos siéntense", así que todos lo hacen. La clase es como una computadora y el alumno ruso es el intérprete. Para JavaScript, el intérprete más común se llama navegador.

NAVEGADOR: cuando se conecta a Internet en una computadora, tableta o teléfono para visitar un sitio web, utiliza un navegador. Algunos ejemplos que puede conocer son Internet Explorer, Chrome, Firefox y Safari. El navegador puede entender JavaScript y decirle a la computadora lo que necesita hacer. Las instrucciones de JavaScript se llaman funciones.

FUNCIÓN: Una función en JavaScript es como una fábrica. Podría ser una pequeña fábrica con solo una máquina adentro. O puede contener muchas otras pequeñas fábricas, cada una con muchas máquinas que realizan diferentes trabajos. En una fábrica de ropa de la vida real, podría tener resmas de tela y bobinas de hilo entrando y camisetas y jeans saliendo. Nuestra fábrica de JavaScript solo procesa datos, no puede coser, perforar un agujero ni fundir metal. En nuestra fábrica de JavaScript, los datos entran y salen datos.

Todo este material de datos suena un poco aburrido, pero es realmente genial; podríamos tener una función que le diga a un robot qué hacer para la cena. Digamos que te invito a ti y a tu amigo a mi casa. Te gustan más las patas de pollo, me gustan las salchichas, tu amigo siempre quiere lo que quieres y mi amigo no come carne.

No tengo tiempo para ir de compras, por lo que la función necesita saber qué tenemos en el refrigerador para tomar decisiones. Cada ingrediente tiene un tiempo de cocción diferente y queremos que todo sea servido caliente por el robot al mismo tiempo. Necesitamos proporcionar a la función los datos sobre lo que nos gusta, la función podría "hablar" con el refrigerador y la función podría controlar el robot.

Una función normalmente tiene un nombre, paréntesis y llaves. Me gusta esto:

function cookMeal() {  /*  STUFF INSIDE THE FUNCTION  */  }

Tenga en cuenta eso /*...*/y //detenga el código que lee el navegador.

NOMBRE: puede llamar a una función casi cualquier palabra que desee. El ejemplo "cookMeal" es típico en unir dos palabras y darle al segundo una letra mayúscula al principio, pero esto no es necesario. No puede tener un espacio y no puede ser un número por sí solo.

PADRES: "Paréntesis" o ()son el buzón en la puerta de la fábrica de funciones de JavaScript o un buzón en la calle para enviar paquetes de información a la fábrica. A veces, el buzón de correo puede estar marcado, por ejemplo cookMeal(you, me, yourFriend, myFriend, fridge, dinnerTime) , en cuyo caso usted sabe qué datos debe proporcionar.

BRAZOS: "Brackets" que se ven así {}son las ventanas tintadas de nuestra fábrica. Desde el interior de la fábrica se puede ver hacia afuera, pero desde afuera no se puede ver hacia adentro.

EL EJEMPLO DE CÓDIGO LARGO ANTERIOR

Nuestro código comienza con la función de palabra , ¡así que sabemos que es uno! Luego, el nombre de la función sing : esa es mi propia descripción de lo que se trata la función. Luego paréntesis () . Los paréntesis siempre están ahí para una función. A veces están vacíos, ya veces tienen algo en Éste tiene una palabra en.: (person). Después de esto hay un aparato ortopédico como este {. Esto marca el inicio de la función sing () . Tiene un compañero que marca el final de sing () como este}

function sing(person) {  /* STUFF INSIDE THE FUNCTION */  }

Entonces, esta función podría tener algo que ver con el canto, y podría necesitar algunos datos sobre una persona. Tiene instrucciones dentro para hacer algo con esos datos.

Ahora, después de la función sing () , cerca del final del código está la línea

var person="an old lady";

VARIABLE: Las letras var significan "variable". Una variable es como un sobre. En el exterior, este sobre está marcado como "persona". En el interior contiene un trozo de papel con la información que necesita nuestra función, algunas letras y espacios unidos como un trozo de cuerda (se llama cadena) que forman una frase que dice "una anciana". Nuestro sobre podría contener otros tipos de cosas como números (llamados enteros), instrucciones (llamadas funciones), listas (llamadas matrices ). Debido a que esta variable se escribe fuera de todos los corchetes {}, y porque puede ver a través de las ventanas polarizadas cuando está dentro de los corchetes, esta variable se puede ver desde cualquier parte del código. Llamamos a esto una 'variable global'.

VARIABLE GLOBAL: persona es una variable global, lo que significa que si cambia su valor de "una anciana" a "un hombre joven", la persona seguirá siendo un hombre joven hasta que decida cambiarlo nuevamente y que cualquier otra función en El código puede ver que es un hombre joven. Presione el F12botón o mire la configuración de Opciones para abrir la consola de desarrollador de un navegador y escriba "persona" para ver cuál es este valor. Escriba person="a young man"para cambiarlo y luego escriba "persona" nuevamente para ver si ha cambiado.

Después de esto tenemos la línea.

sing(person);

Esta línea llama a la función, como si llamara a un perro

"Vamos a cantar , ¡Ven a buscar persona !"

Cuando el navegador haya cargado el código JavaScript y haya alcanzado esta línea, iniciará la función. Puse la línea al final para asegurarme de que el navegador tenga toda la información que necesita para ejecutarlo.

Las funciones definen acciones: la función principal se trata de cantar. Contiene una variable llamada firstPart que se aplica al canto sobre la persona que se aplica a cada uno de los versos de la canción: "Hubo" + persona + "que tragó". Si escribe firstPart en la consola, no obtendrá una respuesta porque la variable está bloqueada en una función: el navegador no puede ver dentro de las ventanas polarizadas de las llaves.

CIERRES: Los cierres son las funciones más pequeñas que están dentro de la función grande sing () . Las pequeñas fábricas dentro de la gran fábrica. Cada uno tiene sus propias llaves, lo que significa que las variables dentro de ellas no se pueden ver desde el exterior. Es por eso que los nombres de las variables ( criatura y resultado ) pueden repetirse en los cierres pero con valores diferentes. Si escribe estos nombres de variables en la ventana de la consola, no obtendrá su valor porque está oculto por dos capas de ventanas polarizadas.

Todos los cierres saben qué es la variable de la función sing () llamada firstPart , porque pueden ver desde sus ventanas tintadas.

Después de los cierres vienen las líneas

fly();
spider();
bird();
cat();

La función sing () llamará a cada una de estas funciones en el orden en que se dan. Entonces se realizará el trabajo de la función sing ().


56

Bien, hablando con un niño de 6 años, posiblemente usaría las siguientes asociaciones.

Imagínese: está jugando con sus hermanos y hermanas pequeños en toda la casa, y se está moviendo con sus juguetes y trajo algunos de ellos a la habitación de su hermano mayor. Después de un tiempo, tu hermano regresó de la escuela y fue a su habitación, y él se encerró dentro de ella, por lo que ahora ya no puedes acceder a los juguetes que quedan allí de manera directa. Pero podrías tocar la puerta y pedirle a tu hermano esos juguetes. Esto se llama cierre de juguete ; tu hermano te lo inventó y ahora está fuera del alcance .

Compare con una situación en la que una puerta estaba cerrada por un tiro y no había nadie adentro (ejecución de la función general), y luego se produce un incendio local y quema la habitación (recolector de basura: D), y luego se construyó una nueva habitación y ahora puede salir hay otros juguetes allí (nueva instancia de función), pero nunca se obtienen los mismos juguetes que se dejaron en la primera instancia de la sala.

Para un niño avanzado, pondría algo como lo siguiente. No es perfecto, pero te hace sentir lo que es:

function playingInBrothersRoom (withToys) {
  // We closure toys which we played in the brother's room. When he come back and lock the door
  // your brother is supposed to be into the outer [[scope]] object now. Thanks god you could communicate with him.
  var closureToys = withToys || [],
      returnToy, countIt, toy; // Just another closure helpers, for brother's inner use.

  var brotherGivesToyBack = function (toy) {
    // New request. There is not yet closureToys on brother's hand yet. Give him a time.
    returnToy = null;
    if (toy && closureToys.length > 0) { // If we ask for a specific toy, the brother is going to search for it.

      for ( countIt = closureToys.length; countIt; countIt--) {
        if (closureToys[countIt - 1] == toy) {
          returnToy = 'Take your ' + closureToys.splice(countIt - 1, 1) + ', little boy!';
          break;
        }
      }
      returnToy = returnToy || 'Hey, I could not find any ' + toy + ' here. Look for it in another room.';
    }
    else if (closureToys.length > 0) { // Otherwise, just give back everything he has in the room.
      returnToy = 'Behold! ' + closureToys.join(', ') + '.';
      closureToys = [];
    }
    else {
      returnToy = 'Hey, lil shrimp, I gave you everything!';
    }
    console.log(returnToy);
  }
  return brotherGivesToyBack;
}
// You are playing in the house, including the brother's room.
var toys = ['teddybear', 'car', 'jumpingrope'],
    askBrotherForClosuredToy = playingInBrothersRoom(toys);

// The door is locked, and the brother came from the school. You could not cheat and take it out directly.
console.log(askBrotherForClosuredToy.closureToys); // Undefined

// But you could ask your brother politely, to give it back.
askBrotherForClosuredToy('teddybear'); // Hooray, here it is, teddybear
askBrotherForClosuredToy('ball'); // The brother would not be able to find it.
askBrotherForClosuredToy(); // The brother gives you all the rest
askBrotherForClosuredToy(); // Nothing left in there

Como puede ver, los juguetes que quedan en la habitación todavía son accesibles a través del hermano y no importa si la habitación está cerrada. Aquí hay un jsbin para jugar con él.


49

Una respuesta para un niño de seis años (suponiendo que sepa qué es una función y qué es una variable y qué datos son):

Las funciones pueden devolver datos. Un tipo de datos que puede devolver de una función es otra función. Cuando se devuelve esa nueva función, todas las variables y argumentos utilizados en la función que la creó no desaparecen. En cambio, esa función principal "se cierra". En otras palabras, nada puede mirar dentro de él y ver las variables que usó, excepto la función que devolvió. Esa nueva función tiene una capacidad especial para mirar hacia atrás dentro de la función que la creó y ver los datos dentro de ella.

function the_closure() {
  var x = 4;
  return function () {
    return x; // Here, we look back inside the_closure for the value of x
  }
}

var myFn = the_closure();
myFn(); //=> 4

Otra forma realmente simple de explicarlo es en términos de alcance:

Cada vez que crea un alcance más pequeño dentro de un alcance más grande, el alcance más pequeño siempre podrá ver lo que está dentro del alcance más grande.


49

Una función en JavaScript no es solo una referencia a un conjunto de instrucciones (como en el lenguaje C), sino que también incluye una estructura de datos ocultos que se compone de referencias a todas las variables no locales que utiliza (variables capturadas). Tales funciones de dos piezas se llaman cierres. Cada función en JavaScript puede considerarse un cierre.

Los cierres son funciones con un estado. Es algo similar a "esto" en el sentido de que "esto" también proporciona el estado para una función, pero la función y "esto" son objetos separados ("esto" es solo un parámetro sofisticado, y la única forma de vincularlo permanentemente a un La función es crear un cierre). Si bien "this" y la función siempre viven por separado, una función no puede separarse de su cierre y el lenguaje no proporciona medios para acceder a las variables capturadas.

Debido a que todas estas variables externas a las que hace referencia una función léxicamente anidada son en realidad variables locales en la cadena de sus funciones léxicamente envolventes (se puede suponer que las variables globales son variables locales de alguna función raíz), y cada ejecución de una función crea nuevas instancias de sus variables locales, se deduce que cada ejecución de una función que devuelve (o de lo contrario la transfiere, como registrarla como una devolución de llamada) una función anidada crea un nuevo cierre (con su propio conjunto potencialmente único de variables no locales referenciadas que representan su ejecución contexto).

Además, debe entenderse que las variables locales en JavaScript no se crean en el marco de la pila, sino en el montón y se destruyen solo cuando nadie hace referencia a ellas. Cuando una función regresa, las referencias a sus variables locales se reducen, pero aún pueden ser no nulas si durante la ejecución actual se convirtieron en parte de un cierre y todavía son referenciadas por sus funciones léxicamente anidadas (lo que puede suceder solo si las referencias a estas funciones anidadas se devolvieron o se transfirieron a algún código externo).

Un ejemplo:

function foo (initValue) {
   //This variable is not destroyed when the foo function exits.
   //It is 'captured' by the two nested functions returned below.
   var value = initValue;

   //Note that the two returned functions are created right now.
   //If the foo function is called again, it will return
   //new functions referencing a different 'value' variable.
   return {
       getValue: function () { return value; },
       setValue: function (newValue) { value = newValue; }
   }
}

function bar () {
    //foo sets its local variable 'value' to 5 and returns an object with
    //two functions still referencing that local variable
    var obj = foo(5);

    //Extracting functions just to show that no 'this' is involved here
    var getValue = obj.getValue;
    var setValue = obj.setValue;

    alert(getValue()); //Displays 5
    setValue(10);
    alert(getValue()); //Displays 10

    //At this point getValue and setValue functions are destroyed
    //(in reality they are destroyed at the next iteration of the garbage collector).
    //The local variable 'value' in the foo is no longer referenced by
    //anything and is destroyed too.
}

bar();

47

Quizás un poco más allá de todos, pero el más precoz de los niños de seis años, pero algunos ejemplos que ayudaron a hacer que el concepto de cierre en JavaScript haga clic para mí.

Un cierre es una función que tiene acceso al alcance de otra función (sus variables y funciones). La forma más fácil de crear un cierre es con una función dentro de una función; La razón es que en JavaScript una función siempre tiene acceso al alcance de la función que lo contiene.

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        alert(outerVar);
    }
    
    innerFunction();
}

outerFunction();

ALERTA: mono

En el ejemplo anterior, se llama a externalFunction, que a su vez llama a innerFunction. Tenga en cuenta que outsideVar está disponible para innerFunction, lo que se evidencia al alertar correctamente el valor de outsideVar.

Ahora considere lo siguiente:

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        return outerVar;
    }
    
    return innerFunction;
}

var referenceToInnerFunction = outerFunction();
alert(referenceToInnerFunction());

ALERTA: mono

referenceToInnerFunction se establece en externalFunction (), que simplemente devuelve una referencia a innerFunction. Cuando se llama a referenceToInnerFunction, devuelve externalVar. De nuevo, como se indicó anteriormente, esto demuestra que innerFunction tiene acceso a externalVar, una variable de externalFunction. Además, es interesante observar que conserva este acceso incluso después de que externalFunction haya terminado de ejecutarse.

Y aquí es donde las cosas se ponen realmente interesantes. Si tuviéramos que deshacernos de externalFunction, digamos establecerlo en nulo, podría pensar que referenceToInnerFunction perdería su acceso al valor de outsideVar. Pero este no es el caso.

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        return outerVar;
    }
    
    return innerFunction;
}

var referenceToInnerFunction = outerFunction();
alert(referenceToInnerFunction());

outerFunction = null;
alert(referenceToInnerFunction());

ALERTA: mono ALERTA: mono

¿Pero cómo es esto? ¿Cómo puede referenceToInnerFunction seguir conociendo el valor de outsideVar ahora que externalFunction se ha establecido en nulo?

La razón por la que referenceToInnerFunction aún puede acceder al valor de outsideVar es porque cuando el cierre se creó por primera vez colocando innerFunction dentro de outsideFunction, innerFunction agregó una referencia al alcance de externalFunction (sus variables y funciones) a su cadena de alcance. Lo que esto significa es que innerFunction tiene un puntero o una referencia a todas las variables de externalFunction, incluida externalVar. Entonces, incluso cuando externalFunction haya terminado de ejecutarse, o incluso si se elimina o se establece en nulo, las variables en su alcance, como externalVar, permanecen en la memoria debido a la referencia sobresaliente a ellas por parte de innerFunction que se ha devuelto a referenceToInnerFunction. Para liberar de la memoria a externalVar y al resto de las variables de externalFunction, tendría que deshacerse de esta excelente referencia a ellas,

//////////

Otras dos cosas sobre los cierres a tener en cuenta. Primero, el cierre siempre tendrá acceso a los últimos valores de su función de contención.

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        alert(outerVar);
    }
    
    outerVar = "gorilla";

    innerFunction();
}

outerFunction();

ALERTA: gorila

Segundo, cuando se crea un cierre, retiene una referencia a todas las variables y funciones de su función de cierre; no puede escoger y elegir. Y, sin embargo, los cierres deben usarse con moderación, o al menos con cuidado, ya que pueden requerir mucha memoria; Se pueden guardar muchas variables en la memoria mucho después de que una función que contiene haya terminado de ejecutarse.


45

Simplemente los señalaría a la página de cierres de Mozilla . Es la mejor, más concisa y simple explicación de los conceptos básicos de cierre y uso práctico que he encontrado. Es muy recomendable para cualquiera que esté aprendiendo JavaScript.

Y sí, incluso lo recomendaría a un niño de 6 años: si el niño de 6 años está aprendiendo acerca de los cierres, entonces es lógico que estén listos para comprender la explicación concisa y simple proporcionada en el artículo.


Estoy de acuerdo: dicha página de Mozilla es particularmente simple y concisa. Sorprendentemente, tu publicación no ha sido tan apreciada como las demás.
Brice Coustillas
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.