¿Qué eres, cierres en JavaScript?

En este artículo intentaré analizar en detalle el mecanismo para implementar cierres en JavaScript. Para esto, usaré el navegador Chrome.

Comencemos con la definición:
Los cierres son funciones que hacen referencia a variables independientes (libres). En otras palabras, la función definida en el cierre 'recuerda' el entorno en el que se creó.
MDN

Si algo no está claro para usted en esta definición, no da miedo. Solo sigue leyendo.

Estoy profundamente convencido de que comprender algo es más fácil y rápido con ejemplos específicos.

Por lo tanto, le sugiero que tome un fragmento de código y lo acompañe con el intérprete de principio a fin en pasos y clasifique lo que está sucediendo.

Entonces comencemos:


Figura 1

Estamos en el contexto global de la llamada, es Global (también conocido como Ventana en el navegador) y vemos que la función principal ya se encuentra en el contexto actual y está lista para funcionar.


Figura 2

Esto sucede porque todas las declaraciones de funciones (en adelante denominadas FD) siempre suben en cualquier contexto, se inicializan inmediatamente y están listas para funcionar. Lo mismo sucede con las variables declaradas a través de var, solo sus valores se inicializan como indefinidos.

También es importante comprender que JavaScript también "eleva" las variables declaradas mediante let y const. La única diferencia es que no los inicializa como var o como FD. Por lo tanto, cuando intentamos acceder a ellos antes de la inicialización, obtenemos un error de referencia.

Además, en main vemos una propiedad internamente oculta [[Scopes]] : esta es una lista de contextos externos a los que main tiene acceso. En nuestro caso, Global está ahí, ya que main se lanza en un contexto global.

El hecho de que en JavaScript la inicialización de referencias al entorno externo se produce en el momento en que se creó la función, y no en el momento de la ejecución, sugiere que JS es un lenguaje con un alcance estático. Y eso es importante.

Adelante


Figura 3

Entramos en la función principal y lo primero que llama la atención es el objeto Local (en la especificación - localEnv). Allí vemos a , ya que esta variable se declara a través de var y 'apareció', bueno, y por tradición vemos los 3 FD (foo, bar, baz). Ahora averigüemos de dónde vino todo.

Cuando se inicia cualquier contexto, se inicia la operación abstracta NewDeclarativeEnvironment , que le permite inicializar LexicalEnvironment (en adelante LE) y VariableEnvironment . Además, NewDeclarativeEnvironment toma 1 argumento: el LE externo, para crear los [[Scopes]] de los que hablamos anteriormente. LE es una API que nos permite definir la relación entre identificadores y variables individuales, funciones. LE consta de 2 componentes:

  1. Entorno de registro : un registro de entorno que le permite determinar la relación entre los identificadores y lo que está disponible para nosotros en el contexto de la llamada actual
  2. Enlace a LE externo. Cada función tiene una propiedad interna [[Scopes]] cuando se crea .

Entorno variable: la mayoría de las veces es lo mismo que LE. La diferencia entre los dos es que el valor de VariableEnvironment nunca cambia, y LE puede cambiar durante la ejecución del código. Para simplificar la comprensión, propongo combinar estos componentes en uno: LE.

También en el Local actual existe esto debido al hecho de que se llamó a ThisBinding ; este también es un método abstracto que inicializa esto en el contexto actual.

Por supuesto, cada FD recibió de inmediato [[Scopes]]:


Figura 4

Vemos que todos los FD recibidos en [[Scopes]] una matriz de [Closure main, Global], lo cual es lógico.

También en la figura vemos Call Stack , esta es una estructura de datos que funciona según el principio de LIFO, último en entrar, primero en salir. Como JavaScript tiene un solo subproceso, solo se puede ejecutar un contexto a la vez. En nuestro caso, este es el contexto de la función principal. Cada nueva llamada de función crea un nuevo contexto, que se apila.

En la parte superior de la pila está siempre el contexto de ejecución actual. Una vez que la función ha completado su ejecución y el intérprete la ha abandonado, el contexto de la llamada se elimina de la pila. Eso es todo lo que necesitamos saber sobre Call Stack en este artículo :)

Resumimos lo que sucedió en el contexto actual:

  • En el momento de la creación, el principal recibió [[Scopes]] con enlaces al entorno externo
  • El intérprete ingresó al cuerpo de la función principal.
  • Call Stack obtuvo el contexto de ejecución main
  • Esta inicializado
  • LE inicializado

De hecho, la parte más difícil ha terminado. Pasamos al siguiente paso en el código:

Ahora necesitamos llamar a baz para obtener el resultado.


Figura 5

Se ha agregado un nuevo contexto de llamada baz a la Pila de llamadas. Vemos que ha aparecido un nuevo objeto de cierre. Aquí obtenemos lo que está disponible para nosotros de [[Scopes]]. Así que llegamos al punto. Este es el cierre. Como puede ver en la Figura 4, Closure (main) va primero en la lista de contextos de 'respaldo' en baz. De nuevo no hay magia.

Llamemos a foo:


Figura 6

Es importante saber que no importa dónde llamemos foo, siempre seguirá los identificadores indefinidos en su cadena [[Scopes]]. A saber, en main y luego en Global, si no se encuentra en main.

Después de ejecutar foo, ella devolvió el valor, y su contexto saltó de Call Stack.
Pasamos a la llamada a la función de barra. En el contexto de la ejecución de barra, hay una variable con el mismo nombre que la variable en LE foo - a . Pero, como ya has adivinado, esto no afecta nada. foo aún tomará el valor de sus [[Scopes]].
El lugar de la llamada no afecta el alcance, solo el lugar de creación
logachyova


Figura 7

Como resultado, baz devolverá 300 y será expulsado de Call Stack. Entonces ocurrirá lo mismo con el contexto principal, nuestro fragmento de código terminará de ejecutarse.

Resumimos:

  • Durante la creación de la función, se establece [[Ámbitos]] . Esto es muy importante para comprender los cierres, ya que el intérprete sigue inmediatamente estos enlaces cuando busca valores
  • Luego, cuando se llama a esta función, se crea un contexto de ejecución activo, que se coloca en la Pila de llamadas
  • ThisBinding se ejecuta y se establece para el contexto actual
  • El LE se inicializa, y todos los argumentos de función, variables declaradas a través de var y FD están disponibles. Además, si hay variables declaradas mediante let o const, también se agregan a LE
  • Si el intérprete no encuentra ningún identificador en el contexto actual, entonces [[Ámbitos]] se utilizan para una búsqueda adicional, que se ordenan por turnos. Si se encuentra el valor, entonces el enlace a él cae en el objeto de cierre especial. Al mismo tiempo, para cada contexto en el que se cierra el actual, se crea un Cierre separado con las variables necesarias
  • Si el valor no se encuentra en ninguno de los ámbitos, incluido Global, se devuelve un error de referencia.

Eso es todo!

Espero que este artículo te haya sido útil y ahora entiendas cómo funciona el mecanismo de bloqueo en JavaScript.

Bye :) Y hasta pronto. Dale me gusta y suscríbete a mi canal :)

Source: https://habr.com/ru/post/459166/


All Articles