Cierres – muchos de ustedes, desarrolladores de JavaScript, probablemente han escuchado este término antes. Cuando comencé mi viaje con JavaScript, me encontré con cierres a menudo. Y creo que son uno de los conceptos más importantes e interesantes de JavaScript.
¿No te parecen interesantes? Esto suele ocurrir cuando no entiendes un concepto: no te parece interesante. (No sé si te pasa o no, pero es mi caso).
Así que en este artículo, intentaré hacer que los cierres sean interesantes para ti.
Antes de adentrarnos en el mundo de los cierres, vamos a entender primero el alcance léxico. Si ya lo conoces, sáltate la siguiente parte. Si no, entra en ella para entender mejor los cierres.
Ambito léxico
Puede que estés pensando – conozco el ámbito local y global, pero ¿qué demonios es el ámbito léxico? Yo reaccioné igual cuando escuché este término. No hay que preocuparse. Vamos a verlo más de cerca.
Es simple como los otros dos ámbitos:
function greetCustomer() { var customerName = "anchal"; function greetingMsg() { console.log("Hi! " + customerName); // Hi! anchal } greetingMsg();}
Puedes ver en la salida anterior que la función interna puede acceder a la variable de la función externa. Esto es alcance léxico, donde el alcance y el valor de una variable se determina por el lugar donde se define/crea (es decir, su posición en el código). ¿Entendido?
Sé que esto último puede haberte confundido. Así que déjame que te profundice. Sabías que el ámbito léxico también se conoce como ámbito estático? Sí, ese es su otro nombre.
También existe el ámbito dinámico, que soportan algunos lenguajes de programación. Por qué he mencionado el ámbito dinámico? Porque puede ayudarte a entender mejor el scoping léxico.
Veamos algunos ejemplos:
function greetingMsg() { console.log(customerName);// ReferenceError: customerName is not defined}function greetCustomer() { var customerName = "anchal"; greetingMsg();}greetCustomer();
¿Estás de acuerdo con la salida? Sí, dará un error de referencia. Esto se debe a que ambas funciones no tienen acceso al ámbito de la otra, ya que están definidas por separado.
Veamos otro ejemplo:
function addNumbers(number1) { console.log(number1 + number2);}function addNumbersGenerate() { var number2 = 10; addNumbers(number2);}addNumbersGenerate();
La salida anterior será 20 para un lenguaje de ámbito dinámico. Los lenguajes que soportan scoping léxico darán referenceError: number2 is not defined
. ¿Por qué?
Porque en el ámbito dinámico, la búsqueda tiene lugar en la función local primero, luego va a la función que llamó a esa función local. Luego busca en la función que llamó a esa función, y así sucesivamente, subiendo por la pila de llamadas.
Su nombre se explica por sí mismo – «dinámico» significa cambio. El alcance y el valor de la variable puede ser diferente, ya que depende de donde se llama a la función. El significado de una variable puede cambiar en tiempo de ejecución.
¿Has entendido el alcance dinámico? Si es así, entonces recuerda que el ámbito léxico es su opuesto.
En el ámbito léxico, la búsqueda tiene lugar en la función local primero, luego va a la función dentro de la cual se define esa función. Luego busca en la función dentro de la cual está definida esa función y así sucesivamente.
Entonces, el ámbito léxico o estático significa que el alcance y el valor de una variable se determina a partir de su definición. No cambia.
Volvamos a ver el ejemplo anterior e intentemos averiguar la salida por nuestra cuenta. Sólo un giro: declara number2
en la parte superior:
var number2 = 2;function addNumbers(number1) { console.log(number1 + number2);}function addNumbersGenerate() { var number2 = 10; addNumbers(number2);}addNumbersGenerate();
¿Sabes cuál será la salida?
Correcto – es 12 para los lenguajes de ámbito léxico. Esto se debe a que primero busca en una función addNumbers
(ámbito más interno) y luego busca hacia adentro, donde está definida esta función. Como obtiene la variable number2
, lo que significa que la salida es 12.
Te estarás preguntando por qué he dedicado tanto tiempo al ámbito léxico aquí. Este es un artículo de cierre, no uno sobre el alcance léxico. Pero si no conoces el ámbito léxico entonces no entenderás los cierres.
¿Por qué? Tendrás la respuesta cuando veamos la definición de un cierre. Así que entremos en materia y volvamos a los closures.
¿Qué es un Closure?
Veamos la definición de un closure:
El closure se crea cuando una función interna tiene acceso a las variables y argumentos de su función externa. La función interna tiene acceso a –
1. Sus propias variables.
2. Las variables y argumentos de la función externa.
3. Las variables globales.
¡Espera! Es esta la definición de cierre o de ámbito léxico? Ambas definiciones parecen iguales. En qué se diferencian?
Pues por eso he definido el alcance léxico más arriba. Porque los closures están relacionados con el scoping léxico/estático.
Vamos a ver de nuevo su otra definición que te dirá en qué se diferencian los closures.
Closure es cuando una función es capaz de acceder a su ámbito léxico, incluso cuando esa función se está ejecutando fuera de su ámbito léxico.
O bien,
Las funciones internas pueden acceder a su ámbito padre, incluso cuando la función padre ya está ejecutada.
¿Confundido? No te preocupes si aún no has pillado el punto. Tengo ejemplos para que lo entiendas mejor. Modifiquemos el primer ejemplo de scoping léxico:
function greetCustomer() { const customerName = "anchal"; function greetingMsg() { console.log("Hi! " + customerName); } return greetingMsg;}const callGreetCustomer = greetCustomer();callGreetCustomer(); // output – Hi! anchal
La diferencia en este código es que estamos devolviendo la función interna y ejecutándola después. En algunos lenguajes de programación, la variable local existe durante la ejecución de la función. Pero una vez ejecutada la función, esas variables locales no existen y no serán accesibles.
Aquí, sin embargo, el escenario es diferente. Una vez ejecutada la función padre, la función interna (función devuelta) puede seguir accediendo a las variables de la función padre. Sí, has adivinado bien. Los cierres son la razón.
La función interna conserva su ámbito léxico cuando la función padre se está ejecutando y, por lo tanto, más tarde esa función interna puede acceder a esas variables.
Para entenderlo mejor, vamos a utilizar el método dir()
de la consola para ver la lista de las propiedades de callGreetCustomer
:
console.dir(callGreetCustomer);
Desde la imagen anterior, se puede ver cómo la función interna conserva su ámbito padre (customerName
) cuando se ejecuta greetCustomer()
. Y posteriormente, utiliza customerName
cuando se ejecuta callGreetCustomer()
.
Espero que este ejemplo te haya ayudado a entender mejor la definición anterior de un closure. Y quizás ahora encuentres los cierres un poco más divertidos.
¿Y ahora qué? Hagamos este tema más interesante viendo diferentes ejemplos.
Ejemplos de cierres en acción
function counter() { let count = 0; return function() { return count++; };}const countValue = counter();countValue(); // 0countValue(); // 1countValue(); // 2
Cada vez que llamas a countValue
, el valor de la variable count se incrementa en 1. Espera – ¿pensabas que el valor de count es 0?
Bueno, eso sería un error ya que un cierre no trabaja con un valor. Almacena la referencia de la variable. Por eso, cuando actualizamos el valor, se refleja en la segunda o tercera llamada y así sucesivamente ya que el closure almacena la referencia.
¿Te sientes un poco más claro ahora? Veamos otro ejemplo:
function counter() { let count = 0; return function () { return count++; };}const countValue1 = counter();const countValue2 = counter();countValue1(); // 0countValue1(); // 1countValue2(); // 0countValue2(); // 1
Espero que hayas adivinado la respuesta correcta. Si no es así, aquí está la razón. Como countValue1
y countValue2
, ambos conservan su propio ámbito léxico. Tienen entornos léxicos independientes. Puedes utilizar dir()
para comprobar el valor de ]
en ambos casos.
Veamos un tercer ejemplo.
Este es un poco diferente. En él, tenemos que escribir una función para conseguir la salida:
const addNumberCall = addNumber(7);addNumberCall(8) // 15addNumberCall(6) // 13
Simple. Utiliza tus recién adquiridos conocimientos sobre cierres:
function addNumber(number1) { return function (number2) { return number1 + number2; };}
Ahora veamos algunos ejemplos complicados:
function countTheNumber() { var arrToStore = ; for (var x = 0; x < 9; x++) { arrToStore = function () { return x; }; } return arrToStore;}const callInnerFunctions = countTheNumber();callInnerFunctions() // 9callInnerFunctions() // 9
Cada elemento del array que almacena una función te dará una salida de 9. ¿Adivinaste bien? Espero que sí, pero aun así déjame decirte la razón. Esto se debe al comportamiento del cierre.
El cierre almacena la referencia, no el valor. La primera vez que se ejecuta el bucle, el valor de x es 0. Luego la segunda vez x es 1, y así sucesivamente. Como el cierre almacena la referencia, cada vez que el bucle se ejecuta está cambiando el valor de x. Y al final, el valor de x será 9. Así que callInnerFunctions()
da una salida de 9.
¿Pero qué pasa si quieres una salida de 0 a 8? Muy sencillo. Utiliza un cierre.
Piénsalo antes de ver la solución de abajo:
function callTheNumber() { function getAllNumbers(number) { return function() { return number; }; } var arrToStore = ; for (var x = 0; x < 9; x++) { arrToStore = getAllNumbers(x); } return arrToStore;}const callInnerFunctions = callTheNumber();console.log(callInnerFunctions()); // 0console.log(callInnerFunctions()); // 1
Aquí, hemos creado un ámbito separado para cada iteración. Puedes usar console.dir(arrToStore)
para comprobar el valor de x en ]
para diferentes elementos del array.
¡Eso es todo! Espero que ahora puedas decir que los cierres te parecen interesantes.
Para leer mis otros artículos, consulta mi perfil aquí.