¿Qué es un cierre de JavaScript? En un lenguaje sencillo, por favor.

Cada función en JavaScript tiene un cierre. Y esta es una de las características más interesantes del lenguaje JavaScript. Porque sin cierres, sería difícil implementar estructuras comunes como devoluciones de llamada o controladores de eventos.

Creas un cierre cada vez que defines una función. Luego, cuando ejecuta funciones, sus cierres les permiten acceder a los datos en sus ámbitos.

Es algo así como cuando se fabrica un coche (definido) se trata con algunas funciones como start, accelerate, decelerate. Estas funciones del automóvil son ejecutadas por el conductor cada vez que opera el automóvil. Los cierres para estas funciones vienen definidos con el propio automóvil y se cierran sobre las variables que necesitan para operar.

Reduzcamos esta analogía a la acceleratefunción. La definición de la función ocurre cuando se fabrica el automóvil:

function accelerate(force) { // Is the car started? // Do we have fuel? // Are we in traction control mode? // Many other checks... // If all good, burn more fuel depending on // the force variable (how hard we’re pressing the gas pedal) }

Cada vez que el conductor presiona el acelerador, esta función se ejecuta. Tenga en cuenta que esta función necesita acceso a muchas variables para funcionar, incluida su propia forcevariable. Pero lo que es más importante, necesita variables fuera de su alcance que estén controladas por otras funciones del automóvil. Aquí es donde el cierre de la acceleratefunción (que conseguimos con el propio coche) viene muy bien.

Esto es lo que accelerateprometió el cierre de la acceleratefunción a la función en sí:

Ok accelerate, cuando se ejecuta, puede acceder a su forcevariable, puede acceder a la isCarStartedvariable, también puede acceder a la fuelLevelvariable y la isTractionControlOnvariable. También puede controlar la currentFuelSupplyvariable que enviamos al motor.

Tenga en cuenta que el cierre no le dio a la acceleratefunción valores fijos para estas variables, sino más bien permiso para acceder a esos valores en el momento en que se ejecuta la función de aceleración.

Los cierres están estrechamente relacionados con los ámbitos de función, por lo que comprender cómo funcionan estos ámbitos le ayudará a comprender los cierres. En resumen, lo más importante que hay que entender sobre los ámbitos es que cuando ejecuta una función, se crea un ámbito de función privada y se utiliza para el proceso de ejecución de esa función.

Luego, estos ámbitos de función se anidan cuando ejecuta funciones desde dentro de funciones (lo que hará todo el tiempo).

Un cierre se crea cuando define una función, no cuando la ejecuta. Luego, cada vez que ejecuta esa función, su cierre ya definido le da acceso a todos los ámbitos de función disponibles a su alrededor.

En cierto modo, puede pensar en los ámbitos como temporales (el ámbito global es la única excepción a esto), mientras que puede pensar en los cierres como permanentes.

Para comprender realmente los cierres y el papel que desempeñan en JavaScript, primero debe comprender algunos otros conceptos simples sobre las funciones de JavaScript y sus alcances.

Antes de comenzar, tenga en cuenta que también he creado un laboratorio interactivo para esto, en el que puede trabajar aquí.

1 - Las funciones se asignan por referencia de valor

Cuando pones una función en una variable como esta:

function sayHello() { console.log("hello"); }; var func = sayHello;

Está asignando a la variable funcuna referencia a la función sayHello, no una copia. Aquí, funces simplemente un alias de sayHello. Todo lo que hagas con el alias lo harás en la función original. Por ejemplo:

func.answer = 42; console.log(sayHello.answer); // prints 42

La propiedad answerse estableció directamente enfuncy leer usando sayHello, que funciona.

También puede ejecutar sayHelloejecutando el funcalias:

func() // prints "hello"

2 - Los ámbitos tienen una vida útil

Cuando llamas a una función, creas un alcance durante la ejecución de esa función. Entonces ese alcance desaparece.

Cuando llama a la función por segunda vez, crea un nuevo alcance diferente durante la segunda ejecución. Entonces este segundo alcance también desaparece.

function printA() { console.log(answer); var answer = 1; }; printA(); // this creates a scope which gets discarded right after printA(); // this creates a new different scope which also gets discarded right after;

Estos dos ámbitos que se crearon en el ejemplo anterior son diferentes. La variable answeraquí no se comparte entre ellos en absoluto.

Cada ámbito de función tiene una vida útil. Se crean y se descartan de inmediato. La única excepción a este hecho es el alcance global, que no desaparece mientras la aplicación se esté ejecutando.

3 - Los cierres abarcan varios ámbitos

Cuando define una función, se crea un cierre

A diferencia de los ámbitos, los cierres se crean cuando define una función, no cuando la ejecuta. Los cierres tampoco desaparecen después de ejecutar esa función.

Puede acceder a los datos en un cierre mucho después de que se defina una función y también después de que se ejecute.

Un cierre engloba todo lo que puede acceder la función definida. Esto significa el alcance de la función definida y todos los alcances anidados entre el alcance global y el alcance de la función definida más el alcance global en sí.

var G = 'G'; // Define a function and create a closure function functionA() { var A = 'A' // Define a function and create a closure function functionB() { var B = 'B' console.log(A, B, G); } functionB(); // prints A, B, G // functionB closure does not get discarded A = 42; functionB(); // prints 42, B, G } functionA();

Cuando definamos functionBaquí, su cierre creado nos permitirá acceder al alcance de functionBmás el alcance de functionAmás el alcance global.

Cada vez que llevamos a cabo functionB, podemos acceder a las variables B, AyGa través de su cierre creado previamente. Sin embargo, ese cierre no nos da una copia de estas variables sino más bien una referencia a ellas. Entonces, si, por ejemplo, el valor de la variable Ase cambia en algún momento después de que functionBse crea el cierre de , cuando ejecutamos functionBdespués de eso, veremos el nuevo valor, no el anterior. La segunda llamada a las functionBimpresiones42, B, Gporque el valor de la variable Ase cambió a 42 y el cierre nos dio una referencia A, no una copia.

No confunda cierres con visores

Es común que los cierres se confundan con los ámbitos, así que asegurémonos de no hacerlo.

// scope: global var a = 1; void function one() { // scope: one // closure: [one, global] var b = 2; void function two() { // scope: two // closure: [two, one, global] var c = 3; void function three() { // scope: three // closure: [three, two, one, global] var d = 4; console.log(a + b + c + d); // prints 10 }(); }(); }();

En el ejemplo simple anterior, tenemos tres funciones y todas se definen y se invocan de inmediato, por lo que todas crean alcances y cierres.

El ámbito de la función one()es su cuerpo. Su cierre nos da acceso tanto a su alcance como al alcance global.

El ámbito de función two()es su cuerpo. Su cierre nos da acceso a su alcance más el alcance de función one()más el alcance global

Y de manera similar, el cierre de la función three()nos da acceso a todos los ámbitos en el ejemplo. Es por eso que pudimos acceder a todas las variables en función three().

Pero la relación entre alcances y cierres no siempre es así de simple. Las cosas se vuelven diferentes cuando la definición y la invocación de funciones ocurren en diferentes ámbitos. Déjame explicarte eso con un ejemplo:

var v = 1; var f1 = function () { console.log(v); } var f2 = function() { var v = 2; f1(); // Will this print 1 or 2? }; f2();

¿Qué crees que imprimirá el ejemplo anterior? El código es simple, f1()imprime el valor de v, que es 1 en el alcance global, pero ejecutamos f1()dentro de f2(), que tiene una diferencia vque es igual a 2. Luego ejecutamos f2().

¿Este código imprimirá 1 o 2?

Si tiene la tentación de decir 2, se sorprenderá. En realidad, este código imprimirá 1. La razón es que los alcances y los cierres son diferentes. La console.loglínea usará el cierre de f1(), que se crea cuando definimos f1(), lo que significa que el cierre de f1()nos da acceso solo al alcance de f1()más el alcance global. El alcance donde ejecutamos f1()no afecta ese cierre. De hecho, el cierre de f1()no nos dará acceso al alcance de f2()en absoluto. Si elimina la vvariable global y ejecuta este código, obtendrá un error de referencia:

var f1 = function () { console.log(v); } var f2 = function() { var v = 2; f1(); // ReferenceError: v is not defined }; f2();

Esto es muy importante de entender y recordar.

4 - Los cierres tienen acceso de lectura y escritura

Dado que los cierres nos dan referencias a variables en ámbitos, el acceso que nos dan significa tanto lectura como escritura, no solo lectura.

Take a look at this example:

function outer() { let a = 42; function inner() { a = 43; } inner(); console.log(a); } outer();

The inner() function here, when defined, creates a closure that gives us access to the variable a. We can read and modify that variable, and if we do modify it, we will be modifying the actual a variable in the outer() scope.

This code will print 43 because we used the inner() function closure to modify the outer() function variable.

This is actually why we can change global variables everywhere. All closures give us both read and write access to all global variables.

5 — Closures can share scopes

Since closures give us access to nested scopes at the time we define functions, when we define multiple functions in the same scope, that scope is shared among all created closures, and of course, because of this, the global scope is always shared among all closures.

function parent() { let a = 10; function double() { a = a+a; console.log(a); }; function square() { a = a*a; console.log(a); } return { double, square } } let { double, square } = parent(); double(); // prints 20 square(); // prints 400 double(); // prints 800

In the example above, we have a parent() function with variable a set to 10. We define two functions in this parent() function’s scope, double() and square(). The closures created for double() and square() both share the scope of the parent() function. Since both double() and square() change the value of a, when we execute the last 3 lines, we double a (making a = 20), then square that doubled value (making a = 400), then double that squared value (making a = 800).

One final test

Let’s now check your understanding of closures so far. Before you execute the following code, try to guess what it will print:

let a = 1; const function1 = function() { console.log(a); a = 2 } a = 3; const function2 = function() { console.log(a); } function1(); function2();

I hope you got that right and I hope these simple concepts will help you to truly understand the significant role function closures play in JavaScript.

Thanks for reading.

Learning React or Node? Checkout my books:

  • Learn React.js by Building Games
  • Node.js Beyond the Basics