Los cierres son uno de los conceptos fundamentales de JavaScript, que causan dificultades a muchos principiantes, que todo programador de JS debe conocer y comprender. Al tener una buena comprensión de los cierres, puede escribir un código mejor, más eficiente y más limpio. Y esto, a su vez, contribuirá a su crecimiento profesional.
El material, cuya traducción publicamos hoy, está dedicado a la historia de los mecanismos internos de los cierres y cómo funcionan en los programas JavaScript.
¿Qué es un cierre?
Un cierre es una función que tiene acceso a un ámbito formado por una función externa en relación con él, incluso después de que esta función externa haya completado su trabajo. Esto significa que un cierre puede almacenar variables declaradas en una función externa y argumentos pasados a ella. Antes de proceder, de hecho, a los cierres, trataremos el concepto de "entorno léxico".
¿Qué es un entorno léxico?
El término "entorno léxico" o "entorno estático" en JavaScript se refiere a la capacidad de acceder a variables, funciones y objetos en función de su ubicación física en el código fuente. Considere un ejemplo:
let a = 'global'; function outer() { let b = 'outer'; function inner() { let c = 'inner' console.log(c); // 'inner' console.log(b); // 'outer' console.log(a); // 'global' } console.log(a); // 'global' console.log(b); // 'outer' inner(); } outer(); console.log(a); // 'global'
Aquí, la función
inner()
tiene acceso a variables declaradas en su propio ámbito, en el ámbito de la función
outer()
y en el ámbito global. La función
outer()
tiene acceso a las variables declaradas en su propio ámbito y en el ámbito global.
La cadena de alcance del código anterior se verá así:
Global { outer { inner } }
Tenga en cuenta que la función
inner()
está rodeada por el entorno léxico de la función
outer()
, que a su vez está rodeada por un ámbito global. Es por eso que la función
inner()
puede acceder a las variables declaradas en la función
outer()
y en el ámbito global.
Ejemplos prácticos de cierres.
Considere, antes de desarmar las complejidades de los circuitos internos, algunos ejemplos prácticos.
▍ Ejemplo No. 1
function person() { let name = 'Peter'; return function displayName() { console.log(name); }; } let peter = person(); peter();
Aquí llamamos a la función
person()
, que devuelve la función interna
displayName()
, y almacenamos esta función en la variable
peter
. Cuando, después de esto, llamamos a la función
peter()
(la variable correspondiente en realidad almacena una referencia a la función
displayName()
), el nombre
Peter
se muestra en la consola.
Al mismo tiempo, no hay una variable
displayName()
en la función
displayName()
, por lo que podemos concluir que esta función puede acceder de alguna manera a la variable declarada en la función externa a ella,
person()
, incluso después de eso cómo funcionó esta función. Quizás esto se deba a que la función
displayName()
es en realidad un cierre.
▍ Ejemplo No. 2
function getCounter() { let counter = 0; return function() { return counter++; } } let count = getCounter(); console.log(count());
Aquí, como en el ejemplo anterior, almacenamos el enlace a la función interna anónima devuelta por la función
getCounter()
en el
count
variable. Como la función
count()
es un cierre, puede acceder a la variable de
counter
de la función
getCount()
incluso después de que la función
getCounter()
haya completado su trabajo.
Tenga en cuenta que el valor de la variable del
counter
no se restablece a 0 cada vez que se llama a la función
count()
. Puede parecer que debería restablecerse a 0, como lo sería cuando se llama a una función regular, pero esto no sucede.
Esto funciona así porque cada vez que se llama a la función
count()
, se crea un nuevo ámbito para ella, pero solo hay un ámbito para la función
getCounter()
. Dado que la variable de
counter
se declara en el alcance de la función
getCounter()
, su valor entre llamadas a la función
count()
se guarda sin restablecer a 0.
¿Cómo funcionan los cortocircuitos?
Hasta ahora, hemos hablado sobre qué son los cierres y hemos examinado ejemplos prácticos. Ahora hablemos de los mecanismos internos de JavaScript que los hacen funcionar.
Para comprender los cierres, debemos tratar con dos conceptos cruciales de JavaScript. Este es el contexto de ejecución y el entorno léxico.
▍ Contexto de ejecución
El contexto de ejecución es un entorno abstracto en el que se calcula y ejecuta el código JavaScript. Cuando se ejecuta el código global, esto sucede dentro del contexto de ejecución global. El código de función se ejecuta dentro del contexto de la ejecución de la función.
En algún momento, el código puede ejecutarse en un solo contexto de ejecución (JavaScript es un lenguaje de programación de un solo subproceso). Estos procesos se gestionan utilizando la llamada Pila de llamadas.
La pila de llamadas es una estructura de datos organizada de acuerdo con el principio LIFO (Última entrada, Primera salida - Última entrada, Primera salida). Los nuevos elementos solo se pueden colocar en la parte superior de la pila, y solo los elementos se pueden eliminar de ella.
El contexto de ejecución actual siempre estará en la parte superior de la pila, y cuando la función actual salga, su contexto de ejecución se extrae de la pila y el control se transfiere al contexto de ejecución, que se encuentra debajo del contexto de esta función en la pila de llamadas.
Considere el siguiente ejemplo para comprender mejor cuál es el contexto de ejecución y la pila de llamadas:
Ejemplo de contexto de ejecuciónCuando se ejecuta este código, el motor de JavaScript crea un contexto de ejecución global para ejecutar el código global, y cuando encuentra una llamada a la
first()
función
first()
, crea un nuevo contexto de ejecución para esta función y lo coloca en la parte superior de la pila.
La pila de llamadas de este código se ve así:
Pila de llamadasCuando se completa la ejecución de la
first()
función
first()
, su contexto de ejecución se recupera de la pila de llamadas y el control se transfiere al contexto de ejecución que se encuentra debajo, es decir, al contexto global. Después de eso, se ejecutará el código restante en el ámbito global.
▍ Ambiente léxico
Cada vez que el motor JS crea un contexto de ejecución para ejecutar una función o código global, también crea un nuevo entorno léxico para almacenar las variables declaradas en esta función durante su ejecución.
El entorno léxico es una estructura de datos que almacena información sobre la correspondencia de identificadores y variables. Aquí, "identificador" es el nombre de una variable o función, y "variable" es una referencia a un objeto (esto incluye funciones) o un valor de un tipo primitivo.
El entorno léxico contiene dos componentes:
- Un registro de entorno es el lugar donde se almacenan las declaraciones de variables y funciones.
- Referencia al entorno externo: un enlace que le permite acceder al entorno léxico externo (principal). Este es el componente más importante que debe tratarse para comprender los cierres.
Conceptualmente, el entorno léxico se ve así:
lexicalEnvironment = { environmentRecord: { <identifier> : <value>, <identifier> : <value> } outer: < Reference to the parent lexical environment> }
Eche un vistazo al siguiente fragmento de código:
let a = 'Hello World!'; function first() { let b = 25; console.log('Inside first function'); } first(); console.log('Inside global execution context');
Cuando el motor JS crea un contexto de ejecución global para ejecutar código global, también crea un nuevo entorno léxico para almacenar variables y funciones declaradas en el ámbito global. Como resultado, el entorno léxico del alcance global se verá así:
globalLexicalEnvironment = { environmentRecord: { a : 'Hello World!', first : < reference to function object > } outer: null }
Tenga en cuenta que la referencia al entorno léxico externo (
outer
) se establece en
null
, ya que el ámbito global no tiene un entorno léxico externo.
Cuando el motor crea un contexto de ejecución para la
first()
función
first()
, también crea un entorno léxico para almacenar las variables declaradas en esta función durante su ejecución. Como resultado, el entorno léxico de la función se verá así:
functionLexicalEnvironment = { environmentRecord: { b : 25, } outer: <globalLexicalEnvironment> }
El enlace al entorno léxico externo de la función se establece en
<globalLexicalEnvironment>
, ya que en el código fuente el código de la función está en el ámbito global.
Tenga en cuenta que cuando la función termina su trabajo, su contexto de ejecución se recupera de la pila de llamadas, pero su entorno léxico puede eliminarse de la memoria o puede permanecer allí. Depende de si en otros entornos léxicos hay referencias a este entorno léxico en forma de enlaces a un entorno léxico externo.
Análisis detallado de ejemplos de trabajo con cierres.
Ahora que nos hemos armado con el conocimiento del contexto de ejecución y el entorno léxico, volveremos a los cierres y analizaremos con mayor profundidad los mismos fragmentos de código que ya examinamos.
▍ Ejemplo No. 1
Eche un vistazo a este fragmento de código:
function person() { let name = 'Peter'; return function displayName() { console.log(name); }; } let peter = person(); peter();
Cuando se ejecuta la función
person()
, el motor JS crea un nuevo contexto de ejecución y un nuevo entorno léxico para esta función. Al finalizar el trabajo, la función devuelve la función
displayName()
, una referencia a esta función se escribe en la variable
peter
.
Su entorno léxico se verá así:
personLexicalEnvironment = { environmentRecord: { name : 'Peter', displayName: < displayName function reference> } outer: <globalLexicalEnvironment> }
Cuando la función
person()
sale, su contexto de ejecución se extrae de la pila. Pero su entorno léxico permanece en la memoria, ya que hay un enlace a él en el entorno léxico de su función interna
displayName()
. Como resultado, las variables declaradas en este entorno léxico permanecen disponibles.
Cuando se llama a la función
peter()
(la variable correspondiente almacena una referencia a la función
displayName()
), el motor JS crea un nuevo contexto de ejecución y un nuevo entorno léxico para esta función. Este entorno léxico se verá así:
displayNameLexicalEnvironment = { environmentRecord: { } outer: <personLexicalEnvironment> }
No hay variables en la función
displayName()
, por lo que su registro de entorno estará vacío. Durante la ejecución de esta función, el motor JS intentará encontrar la variable de
name
en el entorno léxico de la función.
Como la búsqueda no se puede encontrar en el entorno léxico de la función
displayName()
, la búsqueda continuará en el entorno léxico externo, es decir, en el entorno léxico de la función
person()
, que todavía está en la memoria. Allí, el motor encuentra la variable deseada y muestra su valor en la consola.
▍ Ejemplo No. 2
function getCounter() { let counter = 0; return function() { return counter++; } } let count = getCounter(); console.log(count());
El entorno léxico de la función
getCounter()
se verá así:
getCounterLexicalEnvironment = { environmentRecord: { counter: 0, <anonymous function> : < reference to function> } outer: <globalLexicalEnvironment> }
Esta función devuelve una función anónima que se asigna a la variable de
count
.
Cuando se ejecuta la función
count()
, su entorno léxico se ve así:
countLexicalEnvironment = { environmentRecord: { } outer: <getCountLexicalEnvironment> }
Al realizar esta función, el sistema buscará la variable de
counter
en su entorno léxico. En este caso, nuevamente, el registro del entorno de la función está vacío, por lo que la búsqueda de la variable continúa en el entorno léxico externo de la función.
El motor encuentra la variable, la muestra en la consola e incrementa la variable del
counter
, que se almacena en el entorno léxico de la función
getCounter()
.
Como resultado, el entorno léxico de la función
getCounter()
después de la primera llamada a la función
count()
se verá así:
getCounterLexicalEnvironment = { environmentRecord: { counter: 1, <anonymous function> : < reference to function> } outer: <globalLexicalEnvironment> }
Cada vez que se llama a la función
count()
, el motor de JavaScript crea un nuevo entorno léxico para esta función e incrementa la variable del
counter
, lo que conduce a cambios en el entorno léxico de la función
getCounter()
.
Resumen
En este artículo, hablamos sobre qué son los cierres y clasificamos los mecanismos de JavaScript subyacentes. Los cierres son uno de los conceptos fundamentales de JavaScript más importantes, y todos los desarrolladores de JS deberían comprenderlos. Comprender los cierres es uno de los pasos para escribir aplicaciones efectivas y de alta calidad.
Estimados lectores! Si tiene experiencia en el desarrollo de JS, comparta ejemplos prácticos de uso de cierres con principiantes.
