Cuando comencé a aprender sobre las closures, escuché y leí muchas veces explicaciones como esta: "Una closure es una función que retorna otra función, y la función que se retorna usa variables que se definieron en el cuerpo de la función padre", o varias explicaciones que prácticamente se referían a algo similar a esta definición, y aunque en el escenario que se expone en esta definición se forma una closure, la definición como tal no es 100% correcta y hay muchas otras cosas que podemos aprender para profundizar en el uso y comportamiento de las closures.
En este artículo exploramos a fondo qué son las closures, así como otros conceptos del lenguaje JavaScript, como el entorno léxico, que nos ayudarán a comprender el funcionamiento interno de las mismas. Aprenderemos cómo es que estos conceptos se relacionan directamente con los alcances de bloque(scopes) introducidos en la especificación ECMAScript 6, así como la relación que tienen con las closures, y cómo el entender estos mecanismos nos ayudarán a comprender cómo es que al usar closures se pueden mantener "vivas" variables, constantes, parámetros y demás dentro de su alcance, incluso cuando la función donde fueron creadas hayan terminado su ejecución. Si te interesa conocer todo esto te invito a leer hasta al final porque hay mucha información interesante y seguro que algo nuevo aprendes hoy.
Un closure es la combinación de una función agrupada dentro de otra con referencias a su estado adyacente. Veamos el siguiente ejemplo para entenderlo un poco mejor:
En este ejemplo podemos ver que en la función hijo se lee la variable declarada en la función padre, prácticamente, en este ejemplo la closure se crea entre al función hijo y la variable declarada en la función padre. Si has programado con JavaScript o TypeScript durante un tiempo te habrás percatado de que realmente esto se relaciona directamente con otro concepto que también es muy importante desde inicio que es el scope o alcance, y esto mismo se relaciona con los conceptos de Ámbito Léxico y Entorno Léxico.
Antes de conocer lo que son el Ámbito Léxico y el Entorno Léxico, conozcamos el contexto en que estos conceptos fueron concebidos en JavaScript. En JavaScript tradicional, antes de la llegada de la especificación ECMAScript 6(ES6), las variables eran declaradas con la palabra reservada var, con estas variables, JavaScript hacia un proceso de hoisting, el cual "elevaba" la declaración de las variables al inicio de la ejecución del archivo o módulo, lo cual hacía que pudieras usarlas incluso antes de su declaración o inicialización explícita, tal y como se muestra en el siguiente ejemplo:
Esto como te puedes imaginar, podía causar bugs muy complicados de encontrar. Con la llegada de ES6, JavaScript introdujo nuevas formas de declarar variables y constantes, esto como sabemos es con las palabras reservadas let y const, con esto llegaron nuevos conceptos, entre los cuales están las Temporal Dead Zones(TDZ) o zonas muertas temporales, y los scopes o alcances de bloque.
La TDZ es la zona entre el inicio del bloque y la declaración de una variable, la cual prácticamente solo la vemos cuando declaramos una variable con let, ya que al momento de declarar una constante con const debemos obligatoriamente inicializarlas con un valor. Gracias a esto, si intentamos hacer algo parecido a nuestro ejemplo anterior, pero ahora declarando la variable con let en lugar de var, tendremos el siguiente error:
Gracias a la TDZ, ahora si intentamos usar una variable antes de su declaración tendremos este error de Reference Error: cannot access 'x' before initialization. Es importante que sepamos que la TDZ solo es un mecanismo que hace que no se pueda acceder a una variable antes de su declaración, mas no de su inicialización, puesto que como ocurre con las variables declaradas con var, si podemos acceder a ellas incluso antes de su inicialización ya que también JavaScript las inicializa en undefined como se muestra a continuación:
Otro concepto importante que llegó junto con ES6 es el scope o alcance por bloque. Como vimos en nuestro ejemplo usando var, la variable y, declarada dentro de un bloque, podía ser accedida desde fuera de las llaves --las llaves delimitan el bloque(este comportamiento sigue existiendo en JavaScript moderno, por eso ya no se recomienda declarar variables con var). El alcance por bloque se refiere a que las variables o constantes declaradas con let o const, solo pueden ser leídas en el mismo bloque o en bloques adyacentes "hijos", pero las variables declaradas en bloques "hijos" no pueden ser leídas o accedidas desde bloques "padres" o superiores, veamos el siguiente ejemplo para entenderlo mejor:
En este último ejemplo, la variable var1 puede ser accedida desde el bloque del if puesto que es una variable que se declaró en un bloque "padre"(también se puede decir que se declaró en el ámbito externo), sin embargo como vemos al final, cuando intentamos acceder a var2, que está declarada dentro del bloque "hijo"(dentro de las llaves del if), JavaScript nos arroja un ReferenceError, pues como mencionamos antes, los bloques "padre" no pueden acceder a las variables declaradas en los bloques (o ámbitos) "hijo".
Con esto, ahora los bloques en JavaScript crean ámbitos o scopes, como lo vimos en este último ejemplo, el alcance de la variable var2, sólo es dentro del bloque entre las llaves de la sentencia if, por este comportamiento llegaron otros conceptos nuevos que debemos conocer para comprender a fondo las closures, los cuales son el Ámbito/Alcance Léxico(o lexical scope) y el Entorno Léxico(o lexical environment).
Con la llegada de let y const en ES6, como mencionamos, los bloques crean scopes, con esto, podemos definir el Ámbito Léxico como un concepto o regla del lenguaje JavaScript que determina la visibilidad y accesibilidad de las variables según donde se escribieron "físicamente" en el código; por otro lado, el Entorno Léxico es la implementación física que usa el motor de JavaScript para almacenar esas variables y mantener la referencia al ámbito padre.
En otras palabras, el Ámbito Léxico son las reglas del estándar ECMAScript que determinan cuál es el alcance o scope de las variables, mientras que el Entorno Léxico es el lugar físico en memoria donde se almacena la estructura de datos de las variables de una función, el Entorno Léxico se compone de dos elementos: El Registro de Entorno(o Environment Record): Es donde se almacenan las variables, constantes, funciones y parámetros locales o declarados directamente dentro del scope de la función Una referencia externa: La referencia o enlace al entorno léxico adyacente superior (Outer Lexical Environment).
Prácticamente es el entorno léxico lo que permite que las closures puedan crearse y lo que explica cómo funciona a más bajo nivel el alcance de las variables y constantes.
Un dato interesante y curioso es que algunas fuentes en internet mencionan que el entorno léxico de una función es almacenado en una variable oculta llamada [[Environment]].
En este ejemplo, la variable nombre solo es usada dentro de la función hijo y solo se imprime en consola, una vez la función padre termina de ejecutarse, la variable nombre es marcada como lista para limpieza por el recolector de basura(garbage collector) y la liberación de su espacio en memoria, lo que hace que una vez terminada de ejecutarse ya no se pueda volver a acceder a la variable nombre de ninguna manera.
Ahora bien, llegamos a la parte que se suele enseñar comúnmente cuando se explican las closures en JavaScript, las funciones que retornan funciones que tienen una referencia a una variable o constante declarada en la función padre, veamos este ejemplo:
Podemos acceder a la variable nombre incluso cuando la función donde fue declarada ya terminó su ejecución!!! 🤯
