YOU DON’T KNOW JS – 02: Scope & Closures en castellano en pdf

Hola. Os comparto en esta entrada la traducción que he realizado del segundo libro de la serie de libros You don`t know JS donde se describe a fonfo el mecanismo de los cierres y su relación con el alcance. Espero que os guste tanto como a mí.

Recordaros que es una traducción realizada por mí y que puede haber errores de traducción o de otro tipo y os agradecería que me lo hicieséis saber si detectáis uno.

Scope (alcance, ámbito) & Closures (cierres)


1. Que es un cierre?

Uno de los paradigmas más fundamentales de casi todos los lenguajes de programación es la capacidad de almacenar valores en variables, y luego recuperarlos o modificarlos. De hecho, la capacidad de almacenar valores y extraer valores de las variables es lo que da un estado de programa.

Sin tal concepto, un programa podría realizar algunas tareas, pero serían extremadamente limitadas y no terriblemente interesantes.

Pero la inclusión de variables en nuestro programa engendra las preguntas más interesantes que vamos a abordar ahora: ¿dónde viven esas variables? En otras palabras, ¿dónde se almacenan? Y, lo más importante, ¿cómo los encuentra nuestro programa cuando lo necesita?

Estas preguntas se refieren a la necesidad de un conjunto bien definido de reglas para almacenar las variables en algún lugar y para encontrarlas más adelante. Llamaremos a ese conjunto de reglas: Alcance o ámbito.

Pero, ¿dónde y cómo se establecen estas reglas del alcance?

Teoría del Compilador

Puede ser evidente por sí mismo, o puede ser sorprendente, dependiendo de su nivel de interacción con varios lenguajes, pero a pesar de que JavaScript cae dentro de la categoría general de lenguajes “dinámicos” o “interpretados”, es de hecho un lenguaje compilado. No se compila con suficiente antelación, al igual que muchos lenguajes compilados tradicionalmente, ni tampoco son los resultados de la compilación portátiles entre varios sistemas distribuidos.

Pero, sin embargo, el motor JavaScript realiza muchos de los mismos pasos, aunque en formas más sofisticadas de las que comúnmente conocemos, de cualquier compilador de lenguaje tradicional.

En el proceso tradicional de lenguaje compilado, un trozo de código fuente, su programa, se somete a tres pasos típicamente antes de que se ejecute, más o menos llamado “compilación”:

  1. Tokenizing/Lexing: fragmentar una cadena de caracteres en trozos significativos (al lenguaje) llamados tokens(fichas). Por ejemplo, considere el programa: var a = 2;. Este programa probablemente se dividiría en los siguientes tokens: var, a, =, 2 y ;. El espacio en blanco puede o no persistir como un símbolo, dependiendo de si es significativo o no.

Nota: La diferencia entre el tokenizing y el lexing es sutil y académica, pero se centra en si estos token se identifican o no de una manera sin estado o con estado. En pocas palabras, si el token invocara reglas de análisis de estado para determinar si a debe ser considerado un token distinto o sólo una parte de otro token, eso sería lexing.

  1. Análisis sintáctico: coge un flujo (matriz) de tokens y lo convierte en un árbol de elementos anidados, que representan colectivamente la estructura gramatical del programa. Este árbol se llama “AST” (Abstract Syntax Tree o Arbol abstracto sintáctico ).

El árbol para var a = 2; puede comenzar con un nodo de nivel superior llamado VariableDeclaration, con un nodo hijo llamado Identifier (cuyo valor es a) y otro hijo llamado AssignementExpresión que tiene un hijo llamado NumericLiteral(cuyo valor es 2).

  1. Generación de código: el proceso de tomar un AST y convertirlo en código ejecutable, esta parte varía enormemente dependiendo del idioma, la plataforma a la que se dirija, etc.

Por lo tanto, en lugar de entrar en detalles, simplemente nos limitaremos a decir que hay una manera de tomar nuestro AST descrito anteriormente para var a = 2; y convertirlo en un conjunto de instrucciones de máquina para crear una variable llamada realmente a(incluyendo la reserva de memoria, etc.), y luego almacenar un valor en un a.

Nota: Los detalles de cómo el motor maneja los recursos del sistema son más profundos de lo que vamos a indagar, así que daremos por sentado que el motor es capaz de crear y almacenar variables según sea necesario.

El motor JavaScript es mucho más complejo que esos tres pasos, como la mayoría de los compiladores de lenguajes. Por ejemplo, en el proceso de análisis sintáctico y generación de código, existen ciertamente pasos para optimizar el rendimiento de la ejecución, incluyendo el colapso de elementos redundantes, etc.

Así que, estoy describiendo aquí sólo a grandes rasgos. Pero creo que pronto verán por qué estos detalles que cubrimos, incluso a un alto nivel, son relevantes.

Por un lado, los motores JavaScript no se dan el lujo (como otros compiladores de lenguaje) de tener mucho tiempo para optimizar, porque la compilación de JavaScript no ocurre en una compilación que se construye paso a paso, como ocurre con otros lenguajes.

Para JavaScript, la compilación que se produce ocurre, en muchos casos, en microsegundos (o
menos!) antes de ejecutar el código. Para asegurar el rendimiento más rápido, los motores JS utilizan todo tipo de trucos (como JITs, que compilan “perezosos” e incluso hot re-compile, etc.) que están mucho más allá del “alcance” de nuestra discusión aquí.

Digamos, por simplicidad, que cualquier fragmento de JavaScript tiene que ser compilado antes (normalmente justo antes) de que se ejecute. Por lo tanto, el compilador JS tomará el programa var a = 2; y lo compilará primero, y luego estará listo para ejecutarlo, normalmente de inmediato.

Comprender el alcance

La forma en que abordaremos el aprendizaje sobre el alcance es pensar en el proceso en términos de reconversación. Pero, ¿quién está teniendo la conversación?

El Reparto

Conozcamos al elenco de personajes que interactúan para procesar el programa var a = 2; por lo que entendemos sus conversaciones que escucharemos en breve:

  1. Motor: responsable de la compilación y ejecución de nuestro JavaScript de principio a fin.
    programa.
  2. Compilador: uno de los amigos de Engine; maneja todo el trabajo sucio de analizar y generación de código (ver sección anterior).
  3. Alcance: otro amigo de Engine; recopila y mantiene una lista de búsqueda de todos los identificadores (variables) declarados, y hace cumplir un conjunto estricto de reglas sobre cómo son accesibles en los códigos actualmente en ejecución.

Para que entiendas completamente cómo funciona JavaScript, necesitas empezar a pensar como el Engine (y sus amigos) piensan, hacer las preguntas que ellos hacen, y contestar esas mismas preguntas.

Atrás & Alante

Cuando veas el programa var a = 2; lo más probable es que pienses en eso como una frase. Pero así no es como lo ve nuestro nuevo amigo Motor. De hecho, el Motor ve dos frases distintas, una que maneja el Compilador durante la compilación, y otra que maneja el Motor durante la ejecución.

Por lo tanto, vamos a desglosar cómo el Motor y amigos se acercarán al programa var a = 2;.

La primera cosa que el compilador hará con este programa es realizar lexing para dividirlo en tokens, que luego se traducirá en un árbol. Pero cuando el compilador llega a la generación de código, tratará este programa de una manera algo diferente a lo que quizás se supone.

Una suposición razonable sería que el Compilador producirá código que podría resumirse en este pseudo-código: “Asignar memoria para una variable, nombrarla a, luego fija el valor 2 en esa variable.” Desafortunadamente, eso no es del todo exacto.

El compilador procederá como:

  1. Buscar var a, el Compilador le pide la alcance que vea si una variable ya existe para esa colección de alcances en particular. Si es así, el Compilador ignora esta declaración y sigue adelante. De lo contrario, el Compilador pide al alcance que declare una nueva variable llamada a para esa colección de alcances.
  2. El compilador luego produce código para que el Motor lo ejecute más tarde, para manejar la asignación a = 2. El Motor de código ejecutará primero la preguntará al alcance de si hay una variable llamada a “accesible” en la colección de alcance actual. Si es así, el Motor usa esa variable. Si no, el Motor busca en otra parte (ver la sección de alcance anidado más abajo).

Si el Motor eventualmente encuentra una variable, le asigna el valor 2. Si no, el motor lanzarár un error!

Resumiendo: se toman dos acciones distintas para una asignación de variable: Primero, el Compilador declara una variable (si no se ha declarado previamente en el ámbito actual), y segundo, en la ejecución, el Motor busca la variable en el alcance y la asigna a él, si se encuentra.

El Compilador Habla

Necesitamos un poco más de terminología del compilador para continuar con la comprensión.

Cuando el Motor ejecuta el código que el Compilador produjo para el paso (2), tiene que buscar la variable a para ver si ha sido declarada, y esta consulta está consultando al Alcance. Pero el tipo de búsqueda que realiza el Motor afecta al resultado de la búsqueda.

En nuestro caso, se dice que el Motor estaría realizando una búsqueda “LHS” para la variable a.
El otro tipo de búsqueda se llama “RHS”.

Apuesto a que puedes adivinar lo que significan la “L” y la “R”. Estos términos significan “lado izquierdo” y “lado derecho”.

¿Lado… de qué? de una operación de asignación.

En otras palabras, una búsqueda LHS se realiza cuando una variable aparece en el lado izquierdo de una operación de asignación y una búsqueda RHS se realiza cuando una variable aparece en el lado derecho de una operación de asignación.

En realidad, seamos un poco más precisos. Una búsqueda RHS es indistinguible, a nuestros efectos, de una simple búsqueda del valor de alguna variable, mientras que la búsqueda LHS está intentando encontrar el contenedor de variables en sí mismo, para que pueda asignar. De esta manera, RHS no significa realmente “lado derecho de una asignación” per se, sino que, más exactamente, significa “no lado izquierdo”.

Simplificando poco por un momento, también podrías pensar que “RHS” en lugar de eso significa “recuperar su fuente (valor)”, implicando que RHS significa “ir a obtener el valor de…”.

Analizemos esto más detenidamente.

Cuando yo diga:

La referencia a un es una referencia RHS, porque no se está asignando nada a un aquí. En su lugar, estamos buscando obtener el valor de a, para que el valor pueda ser pasado a console.log(..).
Por contraste:

La referencia a a aquí es una referencia LHS, porque realmente no nos importa cuál es el valor actual, simplemente queremos encontrar la variable como un objetivo para la operación de asignación = 2.

Nota: LHS y RHS que significa “lado izquierdo/derecho de una asignación” no significa necesariamente literalmente “lado izquierdo/derecho del operador de asignación =”. Hay varias otras maneras en que las asignaciones suceden, y por lo tanto es mejor pensar conceptualmente en ello como:”quién es el objetivo de la asignación (LHS)” y “quién es el origen de la asignación (RHS)”.

Considere este programa, que tiene referencias de LHS y RHS:

La última línea que invoca foo(..) como llamada de función requiere una referencia RHS a foo, lo que significa,”ve a buscar el valor de foo, y dámelo”. Además, (..) significa que el valor de foo debe ser ejecutado, así que será mejor que sea una función!

Hay una misión sutil pero importante aquí. ¿Lo has visto?

Puede que te hayas perdido el a = 2 implícito en este fragmento de código. Ocurre cuando el valor 2 se pasa como argumento a la función foo(...), en cuyo caso se asigna el valor 2 al parámetro a. Para asignarlo (implícitamente) al parámetro a, se realiza una búsqueda LHS.

También hay una referencia RHS para el valor de a, y ese valor resultante se pasa a console.log(..). console.log(..) necesita una referencia para ejecutarse. Es una búsqueda RHS para console se produce una resolución de propiedad para ver si tiene un método llamado log.

Finalmente, podemos conceptualizar que hay un intercambio LHS/RHS de pasar el valor 2(a través de la búsqueda RHS de a) al log(...). Dentro de la implementación nativa de log(..), podemos asumir que tiene parámetros, el primero de los cuales (quizás llamado arg1) tiene una consulta de referencia LHS, antes de asignarle 2.

Nota: Puede que tenga la tentación de conceptualizar la función declaración función foo(a) {... como una declaración y asignación normal de variables, como var foo y foo = función(a){.... Al hacerlo, sería tentador pensar que esta declaración de función implicaría una consulta a un LHS.

Sin embargo, la diferencia sutil pero importante es que el Compilador maneja tanto la declaración como la definición del valor durante la generación de código, de manera que cuando el Motor está ejecutando código, no hay procesamiento necesario para “asignar” un valor de función a foo. Por lo tanto, no es realmente apropiado pensar en una declaración de función como una tarea de búsqueda del LHS en la forma en que estamos discutiéndola aquí.

Conversación Motor/Alcance

Imaginemos el intercambio anterior (que procesa este fragmento de código) como una conversación.
La conversación sería algo así:

Motor: Hey Alcance, tengo una referencia RHS para foo. ¿Has oído hablar de él?
Alcance: Sí, lo he hecho. El compilador lo declaró hace un segundo. Es una función. Aquí tienes.
Motor: ¡Genial, gracias! Vale, estoy ejecutando a foo.
Motor: Oye, Alcance, tengo una referencia de LHS para un a, ¿alguna vez oíste hablar de él?
Alcance: Sí, lo he hecho. El compilador lo declaró como un parámetro formal a foorecientemente. Aquí tienes.
Motor: Útil como siempre. Gracias de nuevo. Ahora, hora de asignar 2 a a.
Motor: Oye, Alcance, siento molestarte de nuevo. Necesito una búsqueda RHS para la console. ¿Has oído hablar de él?
Alcance: No hay problema, Engine, esto es lo que hago todo el día. Sí, tengo console. Está integrado. Aquí tienes.
Motor: Perfecto. Búsqueda de log(..). Vale, genial, es una función.
Motor: Oye, Alcance. ¿Puede ayudarme con una referencia RHS a un a. Creo que lo recuerdo, pero quiero volver a comprobarlo.
Alcance: Tienes razón, Engine. El mismo tipo, no ha cambiado. Aquí tienes.
Motor: Cool. Pasando el valor de a, que es 2, a log(..).

Cuestionario

Comprueba tu comprensión hasta ahora. Asegúrese de jugar la parte de Motor y tener una “conversación” con el Alcance:

  1. Identifique todas las búsquedas de LHS (hay 3!).
  2. Identifique todas las búsquedas RHS (hay 4!).

Nota: Consulte el capítulo de revisión para las respuestas al cuestionario!

Alcance anidado

Dijimos que el alcance es un conjunto de reglas para buscar variables por su nombre de identificador. Sin embargo, hay, por lo general, más de un alcance a considerar.

Del mismo modo que un bloque o función está anidado dentro de otro bloque o función, los alcances se anidan dentro de otros alcances. Por lo tanto, si una variable no puede ser encontrada en el alcance inmediato, el motor consulta el siguiente alcance de contención exterior, continuando hasta que se encuentra o hasta que se llegfa al alcance exterior (también conocido como alcance global).

Considra lo siguiente:

La referencia RHS para b no se puede resolver dentro de la función foo, pero se puede resolver en el Scope que la rodea (en este caso, el global).

Así que, revisando las conversaciones entre Engine y Scope, oímos por casualidad:

  • Motor: “Oye, alcance de foo, ¿has oído hablar de b? Tengo una referencia de RHS”.
  • Alcance: “No, nunca he oído hablar de él. Ve a buscar”.
  • Motor: “Oye, alcance fuera de foo, oh, tú eres el alcance global, qué bien. ¿Has oído hablar de b? Tengo una referencia de RHS”.
  • Alcance: “Sí, claro que sí. Aquí tienes.”

Las reglas simples para recorrer el alcance anidado: El motor comienza en el alcance que está ejecutando actualmente, busca la variable allí, luego si no se encuentra, sigue subiendo en el nivel superior, y así sucesivamente. Si se alcanza el alcance global más externo, la búsqueda se detiene, tanto si encuentra la variable como si no.

Construir sobre metáforas

Para visualizar el proceso de la resolución anidada del Alcance, quiero que piense en un edificio alto.

El edificio representa el conjunto de reglas de alcance anidado de nuestro programa. El primer piso del edificio representa su Alcance de ejecución actual, dondequiera que se encuentre. El nivel superior del edificio es el alcance global.

Resuelve las referencias de LHS y RHS mirando en su piso actual, y si no lo encuentra, tomando el ascensor al siguiente piso, mirando allí, luego al siguiente, y así sucesivamente. Una vez que llegas al último piso (el alcance global), encuentras lo que buscas o no. Pero tienes que parar a pesar de todo.

Errores

¿Por qué importa si lo llamamos LHS o RHS?

Debido a que estos dos tipos de búsquedas se comportan de forma diferente en las circunstancias en las que la variable aún no ha sido declarada (no se encuentra en ningún Alcance consultado).

Considera esto:

Cuando la búsqueda de RHS se hace por b la primera vez, no se encontrará. Se dice que se trata de una variable “no declarada”, porque no se encuentra en el alcance.

Si una búsqueda de RHS falla en encontrar una variable, en cualquier parte de los alcances anidados, esto resulta en un ReferenceError que es lanzado por el motor. Es importante tener en cuenta que el error es del tipo ReferenceError.

Por el contrario, si el motor está realizando una búsqueda LHS y llega al piso superior (global
Scope) sin encontrarlo, y si el programa no se está ejecutando en “Strict Mode” (https://developer.mozilla.org/es/docs/Web/JavaScript/Referencia/Modo_estricto), entonces el alcance global creará una nueva variable de ese nombre en el alcance global, y se lo devolverá al motor.

“No, no había uno antes, pero fui útil y creé uno para tí.”

El “modo estricto”, que se agregó en ES5, tiene diferentes comportamientos: normal/relaxed/lazy. Uno de estos comportamientos es que no permite la creación automática/implícita de variables globales. En ese caso, no habría ninguna variable en el alcance para devolver desde una búsqueda LHS, y Engine lanzaría un ReferenceErrorsimilar al caso RHS.

Ahora bien, si se encuentra una variable para una búsqueda RHS, pero se intenta hacer algo con su valor que es imposible, como intentar ejecutar como función un valor no funcional, o hacer referencia a una propiedad en un valor nulo o indefinido, entonces Engine lanza un error diferente, llamado TypeError.

ReferenceError es un error de resolución de alcance, mientras que TypeErrorimplica que la resolución de alcance fue exitosa, pero que hubo un intento de acción ilegal/imposible contra el resultado.

Reseña (TL; DR)

El alcance es el conjunto de reglas que determina dónde y cómo se puede buscar una variable (identificador). Esta búsqueda puede ser con el propósito de asignar a la variable, que es una referencia LHS (izquierda), o puede ser con el propósito de recuperar su valor, que es una referencia RHS (derecha).

Las referencias LHS son el resultado de operaciones de asignación. Las asignaciones relacionadas con los alcances pueden ocurrir con el operador = o pasando argumentos a parámetros de función (para asignarlos).

El JavaScript el motor primero compila el código antes de que se ejecute, y al hacerlo, divide las sentencias como var a = 2; en dos pasos separados:

  1. Primero, var a para ser declarada en ese ámbito de aplicación. Esto se realiza al principio, antes de la ejecución el código.
  2. Posteriormente, a = 2 para buscar la variable (Referencia LHS) y asignarla si se encuentra.

Tanto las búsquedas de referencias LHS como RHS comienzan en el alcance que está ejecutando actualmente, y si es necesario (es decir, no encuentran lo que están buscando allí), suben por el alcance anidado, un alcance (piso) a la vez, buscando el identificador, hasta que llegan al global (piso superior) y se detienen, tanto si lo encuentran como si no lo encuentran.

Las referencias RHS no cumplidas dan como resultado errores ReferenceError. Las referencias LHS no cumplidas dan como resultado una creación de ese nombre global automática de manera implícita (si no está en “Strict Mode”), o un ReferenceError (si está en “Strict Mode”).

Respuestas al cuestionario

  1. Identifique todas las búsquedas de LHS (hay 3!).
    c =..., a = 2 (asignación de parámetros implícita) y b =...
  2. Identifique todas las búsquedas RHS (hay 4!).
    foo (2..., = a;, a +.. y .. + b

En el Capítulo 1, definimos “alcance” como el conjunto de reglas que gobiernan cómo el Motor puede buscar una variable por su nombre identificador y encontrarla, ya sea en el Alcance actual o en cualquiera de los alcances anidados que contiene.

Existen dos modelos predominantes de funcionamiento del alcance. El primero de ellos es, con diferencia, el más común, utilizado por la gran mayoría de los lenguajes de programación. Se llama Lexical Scope (alcance léxico), y lo examinaremos en profundidad. El otro modelo, que todavía es utilizado por algunos lenguajes (como Bash scripting, algunos modos en Perl, etc.) se llama Dynamic Scope (alcance dinámico).

El alcance dinámico está cubierto en el Apéndice A. Lo menciono aquí sólo para proporcionar un contraste con el Lexical Scope, que es el modelo de alcance que emplea JavaScript.

2. Lexical Scope

Como discutimos en el Capítulo 1, la primera fase tradicional de un compilador de lenguaje estándar se llama lexing (también conocido como tokenizing). Si recuerdas, el proceso de lexing examina una cadena de caracteres del código fuente y asigna un significado semántico a las fichas como resultado de algún análisis estadístico.

Es este concepto el que proporciona la base para entender cuál es el alcance léxico y de dónde proviene el nombre.

Para definirlo de alguna manera circularmente, el alcance léxico es el alcance que se define en el momento del lexing. En otras palabras, el alcance léxico se basa en donde las variables y bloques de alcance son creados, por usted, en el momento de la escritura, y por lo tanto es (mayormente) “grabado en piedra” para cuando el lexer(el encargado de hacer el lexing) procesa su código.

Nota: Veremos en un poquito más adelante que hay algunas formas de engañar el alcance léxico, modificándolo después de que el lexer ha pasado, pero estos son malinterpretados. Se considera mejor práctica tratar el alcance léxico como sólo léxico y, por lo tanto, de naturaleza exclusivamente autor-tiempo.

Consideremos este bloque de código:

Hay tres alcances anidados inherentes en este ejemplo de código. Puede ser útil pensar
como burbujas dentro de cada uno.

  • Burbuja 1 abarca el ámbito global, y tiene un solo identificador: foo.
  • Burbuja 2 abarca el alcance de foo, que incluye los tres identificadores: a, bar y b.
  • Burbuja 3 abarca el alcance de bar, e incluye sólo un identificador: c.

Las burbujas de ámbito se definen por el lugar donde se escriben los bloques de ámbito, que uno está anidado dentro del otro, etc. En el próximo capítulo discutiremos diferentes unidades de alcance, pero por ahora, asumamos que cada función crea una nueva burbuja de alcance.

La burbuja para bar está enteramente contenida dentro de la burbuja de foo, porque (y sólo porque) ahí es donde elegimos definir la funciòn bar.

Note que estas burbujas anidadas están estrictamente anidadas. No estamos hablando de los diagramas de Venn donde las burbujas pueden cruzar los límites. En otras palabras, ninguna burbuja para alguna función puede existir simultáneamente (parcialmente) dentro de otras dos burbujas de ámbito externo, del mismo modo que ninguna función puede estar parcialmente dentro de cada una de las dos funciones padre.

Búsquedas

La estructura y la ubicación relativa de estas burbujas de alcance le transmite completamente al Motor todos los lugares que necesita buscar para encontrar un identificador.

En el fragmento de código anterior, el Motor ejecuta la sentencia console.log(..) y va en busca de las tres variables a, b y c referenciadas. Primero comienza con la burbuja de alcance interno, el alcance de la función bar(...). No encontrará a allí, por lo que sube un nivel, hacia la siguiente burbuja de alcance más cercana, el alcance de los foo(...). Encuentra a allí, y entonces usa ese a. Lo mismo para b. Pero c, sí lo encuentra dentro de bar(...).

Si hubiera habido una c tanto en el interior de bar(..) como en el interior de foo(..), el console.log(..) habría encontrado y utilizado el de bar(...), nunca hubiese usado el de foo(...).

La búsqueda del alcance se detiene cuando encuentra la primera coincidencia. El mismo nombre de identificador puede especificarse en varias capas de alcance anidado, que se denomina “enmascaramiento” (el identificador interno “enmascara” el identificador externo). Independientemente de los enmascaramientos, la búsqueda del alcance siempre comienza en el alcance más interno que se está ejecutando en ese momento, y hace su camino hacia afuera/hacia arriba hasta la primera coincidencia, y se detiene.

Nota: Las variables globales también son automáticamente propiedades del objeto global (ventana en los navegadores, etc.), por lo que es posible referenciar una variable global no directamente por su nombre léxico, sino indirectamente como una referencia de propiedad del objeto global.

Esta técnica da acceso a una variable global que de otro modo sería inaccesible debido a su enmascaramiento. Sin embargo, no se puede acceder a variables enmascaradas no globales.

Independientemente de dónde se invoque una función o incluso de cómo se invoque, su alcance léxico sólo se define en función de dónde se haya declarado la función.

El proceso de búsqueda de alcance léxico sólo se aplica a los identificadores de primera clase, tales como a, b y c. Si usted tuviera una referencia a foo.bar.baz en un trozo de código, la búsqueda de alcance léxico se aplicaría para encontrar el identificador de foo, pero una vez que localice esa variable, las reglas de acceso a la propiedad del objeto se encargarían de resolver las propiedades de bar y baz, respectivamente.

Léxico Tramposo

Si el alcance léxico se define sólo por el lugar donde se declara una función, que es enteramente una decisión de autor-tiempo, ¿cómo podría haber una forma de “modificar” (también conocida como alcance léxico tramposo) en tiempo de ejecución?

JavaScript tiene dos mecanismos de este tipo. Ambos están igualmente desaconsejados la comunidad más amplia como malas prácticas a utilizar en su código. Pero los argumentos típicos en su contra suelen pasar por alto el punto más importante: engañar al alcance léxico conduce a un rendimiento inferior.

Antes de explicar el tema del rendimiento, sin embargo, veamos cómo funcionan estos dos mecanismos.

eval

La función de eval(...) en JavaScript toma una cadena como argumento, y trata el contenido de la cadena como si realmente hubiera sido un código escrito en ese punto del programa. En otras palabras, puede generar código de forma programática dentro de su código de autor y ejecutar el código generado como si hubiera estado allí en el momento del autor.

Evaluando eval(..) bajo esa perpectiva, debería estar claro cómo eval(...) permite modificar el entorno de alcance léxico haciendo trampa y fingiendo que el código de autor-tiempo estuvo ahí todo el tiempo.

En las líneas subsecuentes de código después de que se haya ejecutado eval(...), el Motor no “sabrá” o “cuidará” que el código anterior en cuestión haya sido interpretado dinámicamente y por lo tanto modificado el entorno de alcance léxico. El motor simplemente realizará sus búsquedas de alcance léxico como siempre lo hace.

Considere el siguiente código:

La cadena "var b = 3;" se trata, en el punto de la llamada a eval(..), como código que estuvo allí todo el tiempo. Debido a que ese código resulta declarar una nueva variable b, modifica el alcance léxico existente de la variable foo(..). De hecho, como se mencionó anteriormente, este código en realidad crea la variable b dentro de foo(...)que enmascara la b que fue declarada en el ámbito externo (global).

Cuando se llama a console.log(..), se encuentra tanto a como b en el ámbito de foo(..), y nunca encuentra el b externo. Así, imprimimos "1,3" en lugar de "1,2"como hubiera sido el caso normalmente.

Nota: En este ejemplo, por el bien de la simplicidad, la cadena de “código” que pasamos era un texto literal fijo. Pero podría haber sido creado programáticamente añadiendo caracteres basados en la lógica de su programa. eval(..) se utiliza generalmente para ejecutar código creado dinámicamente, ya que la evaluación dinámica del código estático a partir de una cadena literal no proporcionaría ningún beneficio real a la autoría directa del código.

Por defecto, si una cadena de código que eval(...) ejecuta contiene una o más declaraciones (ya sean variables o funciones), esta acción modifica el alcance léxico existente en el que reside la eval(...). Técnicamente, eval(...) puede ser invocada “indirectamente”, a través de diversos trucos (más allá de nuestra discusión aquí), lo que la hace ejecutarse en el contexto del alcance global, modificándola así. Pero en cualquier caso, la eval(...) puede modificar en tiempo de ejecución un alcance léxico autor-tiempo.

Nota: cuando eval(...) se usa en un programa en modo estricto opera en su propio ámbito léxico, lo que significa que las declaraciones hechas dentro de la cuando eval()no modifican realmente el ámbito envolvente.

Hay otras facilidades en JavaScript que tienen un efecto muy similar a eval(..).setTimeout(..) y setInterval(..) pueden tomar una cadena para su primer argumento respectivamente, cuyo contenido se evalúa como el código de una función generada dinámicamente. Es un comportamiento antiguo, heredado y desde hace mucho tiempo desacreditado. No lo hagas!

La funciòn constructora new function(...) toma de manera similar una cadena de código en su último argumento para convertirse en una función generada dinámicamente (los primeros argumentos, si los hay, son los parámetros nombrados para la nueva función). Esta sintaxis función-constructor es un poco más segura que la eval(...), pero aún así debería evitarse en su código.

Los casos de uso para generar código dinámicamente dentro de su programa son increíblemente raros, ya que las degradaciones de rendimiento casi nunca valen la pena.

with

La otra (y ahora desaprobada!) característica en JavaScript que engaña alcance léxico es la palabra clave with. Hay múltiples maneras válidas en las que se puede explicar with, pero yo lo explicaré aquí desde la perspectiva de cómo interactúa con y afecta el alcance léxico.

with se explica normalmente como una mano corta para hacer múltiples referencias de propiedades contra un objeto sin repetir la referencia del objeto cada vez.

Por ejemplo:

Sin embargo, hay mucho más aquí que un cómodo acceso directo a la propiedad de objetos. Considera esto:

En este ejemplo de código, se crean dos objetos o1 y o2. Uno tiene una propiedad a y el otro no. La función foo(...) toma un objeto de referencia como argumento, y llama with (obj){... } sobre la referencia. Dentro del bloque with, hacemos lo que parece ser una referencia léxica normal a una variable a, una referencia LHS de hecho (ver Capítulo 1), para asignarle el valor 2.

Cuando pasamos o1, la asignación a = 2 encuentra la propiedad o1.a y le asigna el valor 2, como se refleja en la subsiguiente sentencia consolelog(o1. a). Sin embargo, cuando pasamos en o2, ya que no tiene una propiedad, no se crea tal propiedad, y o2.a permanece indefinido.

Pero entonces observamos un efecto secundario peculiar, el hecho de que una variable global a fue creada por la asignación a = 2. ¿Cómo puede ser esto?

La expresión with toma un objeto, uno que tiene cero o más propiedades, y lo trata como si fuera un alcance léxico completamente separado, y por lo tanto las propiedades del objeto son tratadas como identificadores definidos léxicamente en ese “alcance”.

Nota: A pesar de que un bloque with trata un objeto como un ámbito léxico, una declaración normal de var dentro de ese bloque no se referirá al bloque delo objeto with, sino al ámbito de la función de contención.

Mientras que la función eval(...) puede modificar el alcance léxico existente si toma una cadena de código con una o más declaraciones en ella, la instrucción with realmente crea un alcance léxico completamente nuevo de la nada, del objeto que se le pasa.

Entendido de esta manera, el “alcance” declarado por la sentenvia with cuando pasamos en o1 era o1, y ese “alcance” tenía un “identificador” en él que corresponde al o1. a propiedad. Pero cuando usamos o2 como el “alcance”, no tenía un “identificador” a en el, y por lo tanto se produjeron las reglas normales de búsqueda del identificador LHS (ver Capítulo 1).

Ni el “alcance” de o2, ni el alcance de foo(...), ni el alcance global mismo, tienen un identificador que encontrar, por lo que cuando se ejecuta a = 2, resulta en la creación automática-global (ya que estamos en modo no estricto).

Es un extraño tipo de pensamiento que lleva a ver a with convertir, en tiempo de ejecución, un objeto y sus propiedades en un “alcance” con “identificadores”. Pero esa es la explicación más clara que puedo dar para los resultados que vemos.

Nota: Además de ser una mala idea de usar, tanto eval(...) como with se ven afectadas (restringidas) por el modo estricto. with está totalmente prohibido, mientras que varias formas indirectas o inseguras de eval(...) están prohibidas mientras que mantienen la funcionalidad central.

Rendimiento

Tanto eval(...) como with engañan al ámbito léxico definido por el autor-tiempo modificando o creando un nuevo ámbito léxico en tiempo de ejecución.

Entonces, ¿cuál es el problema, pregunta? Si ofrecen una funcionalidad más sofisticada y flexibilidad de codificación, ¿no son estas buenas características? No.

El motor JavaScript tiene una serie de optimizaciones de rendimiento que realiza durante la fase de compilación. Algunos de estos se reducen a ser capaces de analizar estáticamente el código como su léxico, y predeterminar dónde están todas las declaraciones de variables y funciones, por lo que se requiere menos esfuerzo para resolver los identificadores durante la ejecución.

Pero si el Motor encuentra una eval(...) o with en el código, esencialmente tiene que asumir que todo su conocimiento de la ubicación del identificador puede ser inválido, porque no puede saber, en tiempo de lexing, exactamente qué código puede pasar a eval(...)para modificar el alcance léxico, o el contenido del objeto al que puede pasar para crear un nuevo alcance léxico para ser consultado.

En otras palabras, en el sentido pesimista, la mayoría de las optimizaciones que haría son inútiles si eval(...) o with están presentes, por lo que simplemente no realiza las optimizaciones en absoluto.

Es casi seguro que su código tenderá a ir más lento simplemente por el hecho de que usted incluye un eval(...) o with en cualquier parte del código. No importa lo inteligente que sea el Motor para tratar de limitar los efectos secundarios de estas suposiciones pesimistas, no hay manera de evitar el hecho de que sin las optimizaciones, el código corre más lento.

Resumen (TL;DR)

Ámbito de aplicación léxico significa que el ámbito de aplicación se define mediante decisiones a tiempo de autor de dónde se declaran las funciones. La fase lexing de la compilación es esencialmente capaz de saber dónde y cómo se declaran todos los identificadores, y así predecir cómo se buscarán durante la ejecución.

Dos mecanismos en JavaScript pueden “engañar” el alcance léxico: eval(...) y with. El primero puede modificar el alcance léxico existente (en tiempo de ejecución) evaluando una cadena de “código” que contiene una o más declaraciones. Este último crea esencialmente un nuevo alcance léxico (nuevamente, en tiempo de ejecución) al tratar una referencia de objeto como un “alcance” y las propiedades de ese objeto como identificadores de alcance.

La desventaja de estos mecanismos es que defrauda la capacidad del Motor para realizar optimizaciones de tiempo de compilación con respecto a la búsqueda del alcance, porque el Motor tiene que asumir pesimísticamente que tales optimizaciones serán inválidas. El código se ejecutará más despacio como resultado del uso de cualquiera de las dos características. No los uses.

3. Alcance de Función versus Alcance del bloque

Como hemos explorado en el Capítulo 2, el alcance consiste en una serie de “burbujas” que actúan como un contenedor o cubo, en el que se declaran identificadores (variables, funciones). Estas burbujas anidan ordenadamente una dentro de la otra, y este anidamiento se define en tiempo de autor.

¿Pero qué es exactamente lo que hace una nueva burbuja? ¿Es sólo la función? ¿Pueden otras estructuras en JavaScript crear burbujas de alcance?

Alcance de las funciones

La respuesta más común a estas preguntas es que JavaScript tiene un alcance basado en funciones. Es decir, cada función que declare crea una burbuja para sí misma, pero ninguna otra estructura crea sus propias burbujas de alcance. Como veremos en un momento, esto no es del todo cierto.

Pero primero, exploremos el alcance de la función y sus implicaciones.

Considere este código:

En este fragmento, la burbuja de alcance para foo(..) incluye identificadores a, b , cy bar. No importa dónde aparezca una declaración en el ámbito de aplicación, la variable o función pertenece a la burbuja del ámbito que la contiene, independientemente de ello. Exploraremos cómo funciona exactamente eso en el próximo capítulo.

bar(...) tiene su propia burbuja de alcance. Lo mismo ocurre con el ámbito global, que sólo tiene un identificador: foo.

Debido a que a, b , c, y bar todos pertenecen a la burbuja de alcance de foo(..) , no son accesibles fuera de foo(..). Es decir, el siguiente código resultaría en errores ReferenceError, ya que los identificadores no están disponibles para el alcance global:

Sin embargo, todos estos identificadores (a,b,c,foo y bar) son accesibles dentro de foo y, de hecho, también dentro de bar(..) (asumiendo que no hay declaraciones de identificador de sombra dentro de bar(..)).

El alcance de la función fomenta la idea de que todas las variables pertenecen a la función, y pueden ser usadas y reutilizadas a lo largo de toda la función (y de hecho, accesibles incluso a los ámbitos anidados). Este enfoque de diseño puede ser muy útil, y ciertamente puede hacer pleno uso de la naturaleza “dinámica” de las variables JavaScript para asumir valores de diferentes tipos según sea necesario.

Por otro lado, si no se toman precauciones cuidadosas, las variables que existen a lo largo de la totalidad de un alcance pueden llevar a algunas dificultades inesperadas.

Esconderse en el ámbito simple

La forma tradicional de pensar acerca de las funciones es que usted declara una función, y luego agrega código dentro de ella. Pero el pensamiento inverso es igualmente poderoso y útil: toma cualquier sección arbitraria de código que hayas escrito, y envuelve una declaración de función alrededor de ella, que en efecto “esconde” el código.

El resultado práctico es crear una burbuja de alcance alrededor del código en cuestión, lo que significa que cualquier declaración (variable o función) en ese código estará ahora ligada al alcance de la nueva función de envoltura, en lugar del alcance que antes incluía. En otras palabras, puede “ocultar” variables y funciones incluyéndolas en el ámbito de una función.

¿Por qué sería útil “ocultar” variables y funciones?

Hay una variedad de razones que motivan esta ocultación basada en el alcance. Tienden a surgir del principio de diseño de software “Principio de Mínimo Privilegio“, también llamado a veces “Mínima Autoridad” o “Mínima Exposición”. Este principio establece que en el diseño de software, como la API para un módulo/objeto, debe exponer sólo lo que es mínimamente necesario, y “ocultar” todo lo demás.

Este principio se extiende a la elección del ámbito de aplicación para contener variables y funciones. Si todas las variables y funciones estuvieran en el ámbito global, por supuesto serían accesibles a cualquier ámbito anidado. Pero esto violaría este principio en el sentido de que está (probablemente) exponiendo muchas variables o funciones que de otra manera debería mantener privadas, ya que el uso apropiado del código desalentaría el acceso a esas variables/funciones.

Por ejemplo:

En este fragmento, la variable b y la función doSomethingElse(..) son probablemente detalles “privados” de cómo hace su trabajo doSomething(..). Dar al ámbito de aplicación adjunto “acceso” a b y doSomethingElse(..) no sólo es innecesario sino también posiblemente “peligroso”, en el sentido de que pueden utilizarse de forma inesperada, intencionada o no, y esto puede violar las suposiciones previas de doSomething(..).

Un diseño más “apropiado” escondería estos detalles privados dentro del ámbito de doSomething(...), como:

Ahora, b y doSomethingElse(...) no son accesibles a ninguna influencia externa, sino que están controlados sólo por doSomething(...). La funcionalidad y el resultado final no se han visto afectados, pero el diseño mantiene los detalles privados ocultos, lo que generalmente se considera mejor software.

Prevención de colisión

Otra ventaja de “ocultar” variables y funciones dentro de un ámbito de aplicación es evitar la colisión involuntaria entre dos identificadores diferentes con el mismo nombre pero con usos diferentes. La colisión resulta a menudo en una sobreescritura inesperada de los valores.

Por ejemplo:

La asignación i = 3 dentro de bar(..) sobrescribe, inesperadamente, la i que fue declarada en foo(..) en el bucle for. En este caso, resultará en un bucle infinito, porque i está ajustada a un valor fijo de 3 y que permanecerá para siempre < 10.

La asignación dentro de bar(..) necesita declarar una variable local a utilizar, independientemente del nombre de identificador que se elija. var i = 3; solucionaría el problema (y crearía una declaración de “variable enmascarada” previamente mencionada para i). Una opción adicional, no alternativa, es elegir otro nombre de identificador por completo, como var j = 3; . Pero el diseño de su software puede naturalmente requerir el mismo nombre de identificador, por lo que utilizar el alcance para “ocultar” su declaración interna es su mejor opción en ese caso.

“Espacios de nombres(namespaces)” globales

Un ejemplo particularmente fuerte de colisión de variables (probable) ocurre en el ámbito global. Múltiples bibliotecas cargadas en su programa pueden colisionar fácilmente entre sí si no ocultan adecuadamente sus funciones y variables internas/privadas.

Tales librerías típicamente crearán una única declaración de variables, a menudo un objeto, con un nombre suficientemente único, en el ámbito global. Este objeto se utiliza entonces como un “espacio de nombres” para esa biblioteca, donde todas las exposiciones específicas de funcionalidad se hacen como propiedades de ese objeto (espacio de nombres), en lugar de como identificadores de nivel superior con un alcance léxico.

Por ejemplo:

Gestión de Módulos

Otra opción para evitar colisiones es el enfoque más moderno del “módulo”, que utiliza cualquiera de los diversos gestores de dependencias. Utilizando estas herramientas, ninguna biblioteca añade nunca ningún identificador al ámbito global, sino que se requiere que sus identificadores se importen explícitamente a otro ámbito específico mediante el uso de los diversos mecanismos del administrador de dependencias.

Debe observarse que estas herramientas no poseen una funcionalidad “mágica” exenta de reglas de alcance léxico. Simplemente utilizan las reglas de delimitación como se explica aquí para hacer cumplir que no se inyectan identificadores en ningún ámbito compartido, sino que se mantienen en ámbitos privados, no susceptibles a colisiones, lo que evita cualquier colisión accidental con el ámbito.

Como tal, puede codificar defensivamente y lograr los mismos resultados que los gerentes de dependencia sin necesidad de usarlos, si así lo desea. Consulte el Capítulo 5 para obtener más información sobre el patrón de módulos.

Funciones como alcances

Hemos visto que podemos tomar cualquier fragmento de código y envolver una función alrededor de él, y que efectivamente “esconde” cualquier variable o declaraciones de función internas desde el ámbito externo dentro del ámbito interno de esa función.

Por ejemplo:

Aunque esta técnica “funciona”, no es necesariamente muy ideal. Hay algunos problemas que introduce. La primera es que tenemos que declarar una función nombrada foo(), lo que significa que el propio nombre identificador foo “contamina” el ámbito de aplicación adjunto (global, en este caso). También tenemos que llamar explícitamente a la función por su nombre (foo()) para que el código envuelto realmente se ejecute.

Sería más ideal si la función no necesitara un nombre (o, más bien, el nombre no contaminara el ámbito circundante), y si la función pudiera ejecutarse automáticamente.

Afortunadamente, JavaScript ofrece una solución a ambos problemas.

Desglosemos lo que está pasando aquí.

En primer lugar, observe que la declaración de la función de envoltura comienza con (función... en lugar de sólo función... Aunque esto puede parecer un detalle menor, en realidad es un cambio importante. En lugar de tratar la función como una declaración estándar, la función se trata como una expresión de función.

Nota: La manera más fácil de distinguir entre declaración y expresión es la posición de la palabra “function” en la declaración (no sólo una línea, sino una declaración distinta). Si “function” es lo primero en la sentencia, entonces es una declaración de función. De lo contrario, es una expresión de función.

La diferencia clave que podemos observar aquí entre una declaración de función y una expresión de función se refiere a dónde está ligado su nombre como identificador.

Compara los dos fragmentos anteriores. En el primer fragmento, el nombre foo está englobado en el ámbito adjunto, y lo llamamos directamente con foo(). En el segundo fragmento, el nombre foo no está englobado en el ámbito adjunto, sino que está englobado sólo dentro de su propia función.

En otras palabras, (función foo(){ .. }) como expresión significa que el identificador foo se encuentra sólo en el ámbito que indican los puntos .., no en el ámbito externo. Ocultar el nombre foo dentro de sí mismo significa que no contamina el ambito circundante innecesariamente.

Anónimas vs. Nombradas

Probablemente esté más familiarizado con las expresiones de función como parámetros de devolución de llamada(callbacks), tales como:

Esto se llama “expresión de función anónima”, porque function()... no tiene identificador de nombre. Las expresiones de función pueden ser anónimas, pero las declaraciones de función no pueden omitir el nombre — eso sería gramaticamente ilegal en JS.

Las expresiones de función anónimas son rápidas y fáciles de escribir, y muchas bibliotecas y herramientas tienden a fomentar este estilo idiomático de código. Sin embargo, tienen varios inconvenientes que considerar:

  1. Las funciones anónimas no tienen un nombre útil para mostrar en la traza de pila, lo que puede dificultar la depuración.
  2. Sin nombre, si la función necesita referirse a sí misma, a la recursión, etc., la función obsoleta
    arguments.callee es desafortunadamente necesaria. Otro ejemplo de la necesidad de auto-referencia es cuando una función del manejador de eventos quiere desvincularese después de ejecutarse.
  3. Las funciones anónimas omiten un nombre que a menudo es útil para proporcionar un código más legible/entendible. Un nombre descriptivo ayuda a autodocumentar el código en cuestión.

Las expresiones de funciones en línea son poderosas y útiles — esto no quita el que sean anónimas o nombradas. Proporcionar un nombre para la expresión de la función resuelve con bastante eficacia todos estos inconvenientes, pero no tiene inconvenientes tangibles. La mejor práctica es nombrar siempre las expresiones de función:

Invocar expresiones de función inmediatamente

Ahora que tenemos una función como expresión en virtud de envolverla en un par de paréntesis (), podemos ejecutar esa función añadiendo otros () al final, como (función foo(){.. })(). El primer par de envolventes () hace de la función una expresión, y el segundo () ejecuta la función.

Este patrón es tan común que hace unos años la comunidad acordó un término para él: IIFE, que significa Expresión de Función Invocada Inmediatamente.

Por supuesto, los IIFE no necesitan nombres, necesariamente — la forma más común de IIFE es usar una expresión de función anónima. Aunque ciertamente es menos común, nombrar un IIFE tiene todos los beneficios mencionados anteriormente sobre las expresiones de función anónimas, por lo que es una buena práctica a adoptar.

Hay una ligera variación en la forma IIFE tradicional, que algunos prefieren: (function(){... }()). Mire de cerca para ver la diferencia. En la primera forma, la expresión de la función se envuelve en (), y entonces el par que invoca () está en el exterior justo después de ella. En la segunda forma, el par que invoca () se mueve al interior del par envolvente exterior ().

Estas dos formas son idénticas en funcionalidad. Es una elección puramente estilística el que usted la prefiera.

Otra variación de las IIFE que es bastante común es utilizar el hecho de que son, de hecho, sólo
llamadas de función, y pasar en ella argumento(s).

Por ejemplo:

Pasamos la referencia del objeto window, pero nombramos al parámetro global, de manera que tengamos una delineación estilística clara para referencias globales vs. no globales. Por supuesto, usted puede pasar en cualquier cosa de un ámbito de aplicación que desee, y usted puede nombrar al parámetro (s) como le convenga. En la mayoría de los casos se trata de una elección estilística.

Otra aplicación de este patrón aborda la preocupación (algo raro) de que un identificador undefined se pueda sobreescribir incorrectamente, causando resultados inesperados. Al nombrar un parámetro como indefinido, pero sin pasar ningún valor para ese argumento, podemos garantizar que el identificador undefined es de hecho el valor undefined en un bloque de código:

Otra variación más del IIFE invierte el orden de las cosas, donde la función a ejecutar se da en segundo lugar, después de la invocación y los parámetros para pasarle a ella. Este patrón se utiliza en el proyecto UMD (Universal Module Definition). Algunas personas lo encuentran un poco más limpio de entender, aunque es un poco más repetitivo.

La expresión de la función def se define en la segunda mitad del fragmento, y luego se pasa como parámetro (también llamado def) a la función IIFE definida en la primera mitad del fragmento. Finalmente, se invoca el parámetro def (la función), pasando window como parámetro global.

Bloques como alcances

Mientras que las funciones son la unidad de alcance más común, y ciertamente la más amplia de los enfoques de diseño en la mayoría de los JS en circulación, otras unidades de alcance son posibles, y el uso de estas otras unidades de alcance puede llevar a un código aún mejor, más limpio para mantener.

Muchos otros lenguajes, además de JavaScript, soportan ámbito de bloque, por lo que los desarrolladores de esos lenguajes están acostumbrados a esta mentalidad, mientras que aquellos que han trabajado principalmente con JavaScript pueden encontrar el concepto un poco extraño.

Pero incluso si nunca has escrito una sola línea de código en bloques, es probable que estés familiarizado con este lenguaje tan común en JavaScript:

Declaramos la variable i directamente dentro de la cabeza del bucle for, muy probablemente porque nuestra intención es usar i sólo dentro del contexto de ese for, y esencialmente ignoramos el hecho de que la variable realmente se extiende al ámbito envolvente (función o global).

De eso se trata el ámbito de bloque. Declarar las variables lo más cerca posible, lo más localmente posible, del lugar donde se utilizarán. Otro ejemplo:

Estamos usando una variable bar sólo en el contexto de la sentencia if, por lo que tiene sentido que la declaremos dentro del bloque if. Sin embargo, cuando declaramos variables no es relevante cuando usamos var, porque siempre pertenecerán al ámbito de inclusión. Este fragmento es esencialmente “falso”, por razones estilísticas, y se basa en la autoexigencia para no utilizar accidentalmente la bar en otro lugar dentro de ese ámbito.

El alcance del bloque es una herramienta para extender el anterior “Principio de mínimo privilegio” desde la ocultación de información en funciones hasta la ocultación de información en bloques de nuestro código. Considere de nuevo el ejemplo del bucle for:

¿Por qué contaminar todo el alcance de una función con la variable i que sólo va a ser (o sólo debería ser, al menos) utilizada en el bucle for?

Pero lo que es más importante, es posible que los desarrolladores prefieran comprobar por sí mismos si accidentalmente (re)utilizan variables fuera de su propósito previsto, como cuando se les emite un error sobre una variable desconocida si intentan utilizarla en el lugar equivocado. La delimitación de bloques (si fuera posible) para la variable i haría que esté disponible sólo para el bucle for, causando un error si se accede a i en otra parte de la función. Esto ayuda a asegurar que las variables no sean reutilizadas de manera confusa o difícil de mantener.

Pero, la triste realidad es que, en la superficie, JavaScript no tiene ninguna utilidad para el alcance de bloque.

Eso es, hasta que escarbes un poco más.

with

Aprendimos sobre with en el capítulo 2. Si bien es una construcción mal vista, es un ejemplo de (una forma de) alcance de bloque, en el sentido de que el alcance que se crea a partir del objeto sólo existe durante el tiempo de vida de ese objeto de la declaración with, y no en el alcance adjunto.

try/catch

Es un hecho muy poco conocido que JavaScript en ES3 especificó la declaración de variables en la cláusula catch del try/catch que debe ser ampliada al bloque catch.

Por ejemplo:

Como puedes ver, err existe sólo en la cláusula catch, y lanza un error cuando intentas hacer referencia a ella en otra parte.

Nota: Aunque este comportamiento se ha especificado y es cierto en prácticamente todos los entornos JS estándar (excepto quizás en IE antiguo), muchos “linters” (evaluadores de errores de código) parecen seguir quejándose si tiene dos o más cláusulas catch en el mismo ámbito, cada una de las cuales declara su variable de error con el mismo nombre de identificador. En realidad no se trata de una redefinición, ya que las variables están bloqueadas de forma segura, pero los linters todavía parecen quejarse de este hecho, lo que resulta molesto.

Para evitar estas advertencias innecesarias, algunos desarrolladores nombrarán sus variables de captura err1, err2, etc. Otros simplemente desactivarán el chequeo de errores para nombres de variables duplicados.

La naturaleza del alcance de bloque de catch puede parecer un hecho académico inútil, pero véase el Apéndice B para más información sobre cuán útil podría ser.

let

Hasta ahora, hemos visto que JavaScript sólo tiene algunos comportamientos poco comunes y extraños que exponen la funcionalidad del ámbito de bloque. Si eso fuera todo lo que tuviésemos, y fue así durante muchos, muchos años, entonces la delimitación de bloques no sería muy útil para el desarrollador de JavaScript.

Afortunadamente, ES6 cambia eso, e introduce una nueva palabra clave let que se sitúa junto a var como otra forma de declarar variables.

La palabra clave let adjunta la declaración de variable al alcance de cualquier bloque (comúnmente un par { .. }) en el que esté contenida. En otras palabras, letimplícitamente secuestra el alcance de cualquier bloque para su declaración de variables.

Usar let para adjuntar una variable a un bloque existente es algo implícito. Puede confundir si no estás prestando mucha atención a los bloques que tienen variables de alcance referido a ellos, y están en el hábito de mover los bloques alrededor, envolviéndolos en otros bloques, etc., a medida que desarrollas y evolucionas el código.

La creación de bloques explícitos para la determinación del alcance de los bloques puede abordar algunas de estas preocupaciones, haciendo más obvio dónde se adjuntan y dónde no las variables. Por lo general, el código explícito es preferible al código implícito o sutil. Este estilo explícito de delimitación de bloques es fácil de lograr y se ajusta de forma más natural a cómo funciona la delimitación de bloques en otros idiomas:

Podemos crear un bloque arbitrario para let para vincular simplemente incluyendo un par { .. } en cualquier lugar donde una expresión sea gramaticalmente válida. En este caso, hemos hecho un bloque explícito dentro de la sentencia if, que puede ser más fácil de mover como un bloque entero más tarde en el refactoring, sin afectar a la posición y semántica de la sentencia if adjunta.

Nota: Para otra forma de expresar los alcances explícitos de los bloques, véase el Apéndice B.

En el Capítulo 4, trataremos el tema de la elevación, en el que se habla de que las declaraciones se consideran existentes para todo el ámbito en el que se producen.

Sin embargo, las declaraciones hechas con let no se elevarán a todo el alcance del bloque en el que aparecen. Tales declaraciones no “existirán” de forma observable en el bloque hasta la sentencia de declaración.

Recolección de Basura

Otra razón por la que el alcance del bloque es útil se relaciona con los cierres y la recolección de basura para recuperar la memoria. Vamos a ilustrar brevemente aquí, pero el mecanismo de cierre se explica en detalle en el Capítulo 5.

Considere:

La función de callback click no necesita la variable someReallyBigData en absoluto. Esto significa que, teóricamente, después de que se ejecute el process(...), la gran estructura de datos cargada de memoria (someReallyBigData) podría ser basura recolectada. Sin embargo, es bastante probable (aunque depende de la implementación) que el motor JS todavía tenga que mantener la estructura alrededor, ya que la función click tiene un cierre en todo el alcance.

La delimitación de bloques puede abordar esta preocupación, aclarando al motor que no necesita mantener someReallyBigData a mano:

Declarar bloques explícitos para que las variables sean enlazadas localmente es una poderosa herramienta que puede añadir a su caja de herramientas de código.

Bucles let

Un caso particular donde let destaca es en el caso del bucle for como hemos discutido anteriormente.

No sólo deja entrar el encabezado del bucle for que une la i al cuerpo del bucle, sino que de hecho, la vuelve a unir a cada iteración del bucle, asegurándose de reasignarle el valor del final de la iteración del bucle anterior.

He aquí otra forma de ilustrar el comportamiento de unión por iteración que se produce:

La razón por la cual este enlazamiento por iteración es interesante se aclarará en el Capítulo 5 cuando discutamos los cierres.

Debido a que las declaraciones let se adjuntan a bloques arbitrarios en lugar de al alcance de la función (o al global), puede haber errores(o problemas) donde el código existente tiene una dependencia oculta de las declaraciones var con alcance de función, y reemplazar el var con let puede requerir un cuidado adicional al refactorizar el código.

Considere:

Este código es bastante fácil de refactorizar como:

Pero, tenga cuidado con estos cambios cuando utilice variables de alcance de bloque:

Vea el Apéndice B para un estilo alternativo (más explícito) de determinación del alcance del bloque que puede proporcionar un código de mantenimiento/refactorización más fácil de mantener y más robusto para estos escenarios.

const

Además de let, “ES6” introduce const, que también crea una variable de alcance de bloque, pero cuyo valor es fijo (constante). Cualquier intento de cambiar ese valor en un momento posterior resulta en un error.

Resumen

Las funciones son la unidad de alcance más común en JavaScript. Las variables y funciones que se declaran dentro de otra función están esencialmente “ocultas” de cualquiera de los “alcances” adjuntos, que es un principio de diseño intencional de un buen software.

Pero las funciones no son de ninguna manera la única unidad de alcance. El alcance del bloque se refiere a la idea de que las variables y funciones pueden pertenecer a un bloque arbitrario (generalmente, cualquier par { .. } de código), en lugar de sólo a la función envolvente.

A partir de ES3, la estructura try/catch tiene un alcance de bloque en la cláusula catch.

En ES6, se introduce la palabra clave let (un primo de la palabra clave var) para permitir declaraciones de variables en cualquier bloque de código arbitrario. if(..) { let a = 2; } declarará una variable a que esencialmente secuestra el alcance del bloque al if {.. } y se adhiere allí.

Aunque algunos parecen creer que sí, el ámbito de bloque no debe tomarse como un reemplazo absoluto del ámbito de función var. Ambas funcionalidades coexisten, y los desarrolladores pueden y deben utilizar técnicas de alcance de función y de alcance de bloque cuando sea apropiado para producir un código mejor, más legible/mantenible.

4. Izado

A estas alturas, usted debería estar bastante cómodo con la idea del alcance, y cómo las variables son de los diferentes niveles de ámbito de aplicación en función de dónde y cómo se declaren. Ambos, el alcance de función y el alcance de bloque, se comportan según las mismas reglas en este sentido: cualquier variable declarada en un ámbito se adjunta a dicho ámbito.

Pero hay un detalle sutil de cómo funciona el asignamiento de alcance con las declaraciones que aparecen en varios lugares dentro de un alcance, y ese detalle es lo que examinaremos aquí.

¿La gallina o el huevo?

Existe la tentación de pensar que todo el código que se ve en un programa JavaScript es interpretado línea por línea, en orden descendente, a medida que el programa se ejecuta. Mientras que eso es sustancialmente cierto, hay una parte de esa suposición que puede llevar a un pensamiento incorrecto sobre su programa.

Considere este código:

¿Qué espera que se imprima en la sentencia ‘console.log(...)?

Muchos desarrolladores esperarían un undefined, ya que la sentencia var a viene después de a = 2, y parecería natural asumir que la variable es redefinida, y por lo tanto se le asigna undefined por defecto. Sin embargo, la salida será 2.

Considere otra pieza de código:

Es posible que tenga la tentación de suponer que, dado que el fragmento anterior presentaba un comportamiento abajo-hacia-ariba, tal vez en este fragmento, también se imprimirá 2. Otros pueden pensar que ya que una variable a es usada antes de ser declarada, esto debe resultar en un ReferenceError.

Desafortunadamente, ambas suposiciones son incorrectas. la salida es undefined.

Entonces, ¿qué está pasando aquí? Parece que tenemos una pregunta de “la gallina o el huevo”. ¿Qué es lo primero, la declaración (“huevo”), o la cesión (“gallina”)?

El compilador ataca de nuevo

Para responder a esta pregunta, necesitamos volver al Capítulo 1 y nuestra discusión sobre compiladores. Recuerde que el motor realmente compilará su código JavaScript antes de que se interprete. Parte de la fase de compilación consiste en buscar y asociar todas las declaraciones con sus alcances apropiados. El Capítulo 2 nos mostró que éste es el corazón de Lexical Scope.

Por lo tanto, la mejor manera de pensar las cosas es que todas las declaraciones, tanto variables como funciones, son procesados primero, antes de que cualquier parte de su código sea ejecutada.

Cuando veas var a = 2; probablemente pienses en eso como una declaración. Pero JavaScript en realidad piensa en ello como dos declaraciones: var a; y a = 2; . La primera declaración, se procesa durante la fase de compilación. La segunda declaración, se mantiene en la fase de ejecución.

Nuestro primer fragmento debería ser considerado como que es manejado de esta manera:

…donde la primera parte es la compilación y la segunda parte es la ejecución.

Del mismo modo, nuestro segundo fragmento se procesa como:

Entonces, una forma de pensar, metafóricamente, sobre este proceso, es que la declaración de esa variable y las declaraciones de función se “mueven” desde donde aparecen en el flujo del código hacia arriba del código. Esto da lugar al nombre de “Elevación”.

En otras palabras, el huevo (declaración) viene antes que la gallina (asignación).

Nota: Sólo se izan las declaraciones en sí mismas, mientras que cualquier asignación u otra lógica ejecutable se dejan en su lugar. Si la elevación fuera a reorganizar la lógica ejecutable de nuestro código, eso podría causar estragos.

La declaración de la función foo (que en este caso incluye el valor implícito de ella como la función actual), es izada de forma que se pueda ejecutar la llamada en la primera línea.

También es importante tener en cuenta que la elevación es por alcance. Así que mientras nuestros fragmentos anteriores eran simplificadas en el sentido de que sólo incluían un alcance global, la función foo(..) que estamos ahora examinando muestra que var aes izado hasta la cima de foo(..) (no, obviamente, a la parte superior del programa). Así que el programa tal vez pueda ser interpretado con mayor precisión de esta manera:

Las declaraciones de función son izadas, como acabamos de ver. Pero las expresiones de función no.

La variable identificadora foo se iza y se adjunta al ámbito global de este programa, por lo que foo() no falla como ReferenceError. Pero foo todavía no tiene valor (como lo tendría si hubiera sido una verdadera declaración de función en lugar de una expresión). Por lo tanto, foo() está intentando invocar el valor undefined, que es una operación ilegal TypeError.

También recuerde que aunque es una expresión de función con nombre, el identificador de nombre no está disponible en el ámbito adjunto:

Este fragmento se interpreta con mayor precisión (con elevación) como:

Funciones Primero

Se alzan tanto las declaraciones de función como las declaraciones de variables. Pero un detalle sutil (que puede aparecer en el código con múltiples declaraciones “duplicadas”) es que las funciones son izadas primero, y luego las variables.

Considere:

Se imprime 1 en lugar de 2! Este fragmento es interpretado por el motor como:

Note que var foo era una declaración duplicada (y por lo tanto ignorada), aunque venía antes de la declaración function foo()..., porque las declaraciones de función se alzan antes que las variables normales.

Mientras que múltiples declaraciones var (duplicadas) se ignoran de hecho, las declaraciones de función posteriores sustituyen a las anteriores.

Mientras que todo esto puede parecer como nada más que trivialidades académicas interesantes, resalta el hecho de que las definiciones duplicadas en el mismo ámbito son una idea realmente mala y a menudo conducirán a resultados confusos.

Las declaraciones de función que aparecen dentro de los bloques normales típicamente se elevan en el ámbito envolvente, en lugar de ser condicionales como implica este código:

Sin embargo, es importante tener en cuenta que este comportamiento no es fiable y está sujeto a cambios en futuras versiones de JavaScript, por lo que probablemente sea mejor evitar declarar funciones en bloques.

Revisión (TL;DR)

Podemos estar tentados de ver var a = 2; como una declaración, pero el motor JavaScript no lo ve de esa manera. Ve var a y a = 2 como dos sentencias separadas, la primera una tarea de fase de compilador, y la segunda una tarea de fase de ejecución.

Esto lleva a que todas las declaraciones de un alcance, independientemente de dónde aparezcan, se procesen primero antes de que se ejecute el propio código. Puede visualizar esto como declaraciones (variables y funciones) siendo “movidas” a la parte superior de sus respectivos ámbitos, que llamamos “Izado”.

Se izan las declaraciones mismas, pero las asignaciones, incluso las asignaciones de expresiones de funciones, no se izan.

Tenga cuidado con las declaraciones duplicadas, especialmente las que se mezclan entre declaraciones var normales. y declaraciones de funciones – ¡el peligro le espera si lo hace!

5. Alcance de cierre

Llegamos a este punto con la esperanza de tener una comprensión muy sana y sólida de cómo funciona el alcance.

Dirigimos nuestra atención a una parte del lenguaje increíblemente importante, pero persistentemente elusiva, casi mitológica: el cierre. Si han seguido nuestro debate sobre el alcance léxico hasta ahora, el resultado es que el cierre va a ser, en gran medida, predecible, casi obvio. Hay un hombre detrás de la cortina del mago, y estamos a punto de verlo. ¡No, su nombre no es Crockford!

Sin embargo, si usted tiene preguntas molestas sobre el alcance léxico, ahora sería un buen momento para volver atrás y revisar el Capítulo 2 antes de proceder.

El esclarecimiento

Para aquellos que tienen un poco de experiencia en JavaScript, pero que tal vez nunca han llegado completamente comprensión de los cierres puede parecer un nirvana especial que uno debe esforzarse y sacrificarse para alcanzarlo.

Recuerdo años atrás cuando tenía un firme conocimiento de JavaScript, pero no tenía idea de lo que era el cierre. La insinuación de que había esta otra cara del lenguaje, una que prometía aún más de lo que ya poseía, se burlaba de mí. Recuerdo haber leído en el código fuente de los primeros marcos de trabajo tratando de entender cómo funcionaba realmente. Recuerdo la primera vez que algo del “patrón modular” comenzó a emerger en mi mente. Recuerdo el ¡a-ja! del momento muy claramente.

Lo que no sabía entonces, es que me llevó años entenderlo, y lo que espero impartir a usted en este momento, es este secreto: el cierre está a su alrededor en JavaScript, sólo tiene que reconocerlo y abrazarlo. Los cierres no son una herramienta especial de inclusión voluntaria para la cual usted debe aprender nuevas sintaxis y patrones. No, los cierres ni siquiera son un arma que debes aprender a manejar y dominar como Lucas se entrenó en La Fuerza

Los cierres ocurren como resultado de escribir código que se basa en el alcance léxico. Simplemente ocurren. Ni siquiera es necesario crear cierres de forma intencionada para sacar provecho de ellos. Los cierres son creados y usados para usted en todo su código. Lo que te falta es el contexto mental apropiado para reconocer, abrazar y aprovechar los cierres por voluntad propia.

El momento de la iluminación debería ser: oh, los cierres ya están ocurriendo por todo mi código, finalmente puedo verlos ahora. Entender los cierres es como cuando Neo ve Matrix por primera vez.

El quid de la cuestión

Basta de hipérboles y referencias a películas.

He aquí una definición sucia de lo que usted necesita saber para entender y reconocer los cierres:

El cierre es cuando una función es capaz de recordar y acceder a su ámbito léxico incluso cuando esa función se está ejecutando fuera de su ámbito léxico.

Vamos a entrar en algún código para ilustrar esa definición.

Este código debe parecer familiar debido a nuestras discusiones de Alcance Anidado. La función bar() tiene acceso a la variable a en el ámbito envolvente exterior debido a las reglas de búsqueda del ámbito léxico (en este caso, es una búsqueda de referencia RHS).

¿Esto es un “cierre”?

Bueno, técnicamente… quizás. Pero para nuestra definición de lo que necesitas saber… no exactamente. Creo que la manera más precisa de explicar la referencia a bar() es a través de reglas de búsqueda de alcance léxico, y esas reglas son sólo (¡una parte importante!) de lo que es el cierre.

Desde una perspectiva puramente académica, lo que se dice del fragmento anterior es que la función bar() tiene un cierre sobre el alcance de foo() (y de hecho, incluso sobre el resto de los alcances a los que tiene acceso, como el alcance global en nuestro caso). Dicho de otra manera, se dice que bar() se cierra sobre el alcance de foo(). Porque bar() aparece anidado dentro de foo(). Simple y llanamente.

Pero, el cierre definido de esta manera no es directamente observable, ni vemos el cierre ejercido en ese fragmento. Vemos claramente el alcance léxico, pero el cierre sigue siendo una especie de misteriosa sombra cambiante detrás del código.

Consideremos entonces el código que saca a la luz el cierre:

La función bar() tiene un ámbito de acceso léxico al ámbito interno de foo(). Pero entonces, tomamos bar(), la función en sí misma, y la pasamos como un valor. En este caso, se devuelve (return bar) el objeto de función en sí que hace referencia a bar().

Después de ejecutar foo(), asignamos el valor que devolvió (nuestra función bar()interna) a una variable llamada baz, y luego invocamos baz(), que entonces está invocando nuestra función bar() interna, sólo que por una referencia de identificador diferente.

bar() se ejecuta, por supuesto. Pero en este caso, se ejecuta fuera de su ámbito léxico declarado.

Después de ejecutar foo(), normalmente esperaríamos que todo el alcance interno de foo() desaparezca, porque sabemos que el Motor emplea un Recolector de Basura que viene y libera la memoria una vez que ya no está en uso. Puesto que parece que el contenido de foo() ya no está en uso, parecería natural que se considere que ha desaparecido.

Pero la “magia” de los cierres no permite que esto suceda. De hecho, ese ámbito interior todavía está “en uso” y, por lo tanto, no desaparece. ¿Quién lo está usando? La propia función bar().

En virtud del lugar donde fue declarado, bar() tiene un cierre de alcance léxico sobre ese alcance interno de foo(), lo que mantiene ese alcance vivo para que bar() haga referencia en cualquier momento posterior.

bar() todavía tiene una referencia a ese alcance, y esa referencia se llama cierre. Así, unos microsegundos más tarde, cuando se invoca la variable baz (invocando la función interna que inicialmente denominamos bar), tiene acceso al ámbito léxico en tiempo de autor, por lo que puede acceder a la variable a tal y como esperábamos.

La función está siendo invocada fuera de su ámbito léxico de tiempo de autor. El cierre permite que la función continúe accediendo al ámbito léxico en el que fue definida en tiempo de autor.

Por supuesto, cualquiera de las diversas formas en que las funciones pueden ser transmitidas como valores, y de hecho invocadas en otros lugares, son todos ejemplos de observación/ejercicio del cierre.

Pasamos la función interna baz sobre bar, y llamamos a esa función interna (etiquetada como fn ahora), y cuando lo hacemos, se observa su cierre sobre el alcance interno de foo(), accediendo a a.

Estos pases alrededor de las funciones pueden ser indirectos, también.

Cualquiera que sea el mecanismo que utilicemos para transportar una función interna fuera de su alcance léxico, mantendrá una referencia de alcance a donde fue declarada originalmente, y dondequiera que la ejecutemos, ese cierre será ejercido.

Ahora puedo ver

Los fragmentos de código anteriores son algo académicos y construidos artificialmente para ilustrar el uso del cierre. Pero te prometí algo más que un nuevo juguete. Prometí que el cierre era algo a tu alrededor en tu código existente. Veamos ahora esa realidad.

Tomamos una función interna (llamada timer) y la pasamos a setTimeout(...). Pero timer tiene un cierre de alcance sobre el alcance de wait(...), de hecho manteniendo y usando una referencia a la variable menssage.

Mil milisegundos después de haber ejecutado wait(...), su alcance interno debería haber desaparecido hace mucho tiempo, pero la función timer interna todavía tiene cierre sobre ese alcance.

En el fondo de las entrañas del motor, la utilidad incorporada setTimeout(..) tiene referencia a algún parámetro, probablemente llamado fn o func o algo así. El motor va a invocar esa función, que es invocar nuestro timer interior y la referencia del alcance léxico es sigue intacto.

Cierre.

Si te ha persuadido de jQuery (o de cualquier framework de JS):

No estoy seguro de qué tipo de código escribes, pero regularmente escribo código que es responsable de controlar todo un ejército global de robots de cierre, ¡así que esto es totalmente realista!

Esencialmente, cuando y donde quiera que se traten las funciones (que acceden a sus respectivos alcances léxicos) como valores de primera clase y los pasen de un lado a otro, es probable que veas a esas funciones ejercitando el cierre. Ya sean temporizadores, manejadores de eventos, peticiones Ajax, mensajes entre ventanas, web workers, u otros cualquiera de tipo asíncronos (o sincrónos!), cuando pases en una función de devolución de llamada, prepárate para lanzar algún cierre alrededor!

Nota: El Capítulo 3 introdujo el patrón IIFE. Aunque a menudo se dice que la IIFE (por sí misma) es un ejemplo de cierre observado, no estoy de acuerdo con nuestra definición anterior.

Este código “funciona”, pero no es estrictamente una observación de cierre. Por qué? Porque la función (que aquí denominamos “IIFE”) no se ejecuta fuera de su ámbito léxico. Se sigue invocando allí mismo en el mismo ámbito que se declaró (el ámbito de inclusión/global que también contiene una a). a se encuentra a través de la búsqueda normal del léxico, no realmente a través del cierre.

Aunque técnicamente el cierre podría estar ocurriendo en el momento de la declaración, no es estrictamente observable, y por lo tanto, como dice el dicho, es un árbol que cae en el bosque sin nadie alrededor para escucharlo.

Aunque un IIFE no es en sí mismo un ejemplo de cierre, crea absolutamente un alcance, y es una de las herramientas más comunes que usamos para crear un alcance que puede ser cerrado. Por lo tanto, los IIFEs están en efecto fuertemente relacionados con el cierre, aunque no ejerzan el cierre ellos mismos.

Deje este libro ahora mismo, querido lector. Tengo una tarea para ti. Ve a abrir algunos de tus códigos JavaScript recientes. Busca tus funciones como valores e identifica dónde estás ya están usando cierres y tal vez ni siquiera lo sabías antes.

Esperaré.

Ahora… ¡ya ves!

Cierre y blucles

El ejemplo canónico más común usado para ilustrar el cierre tiene que ver con el humilde bucle for.

Nota: Los Linters (correctores de código) a menudo se quejan cuando usted pone funciones dentro de los bucles, porque los errores de no entender el cierre son tan comunes entre los desarrolladores. Le explicamos cómo hacerlo aprovechando todo el poder del cierre. Pero esa sutileza se pierde a menudo en los linters y se quejarán de todas formas, asumiendo que no sabes lo que estás haciendo.

El espíritu de este fragmento de código es que normalmente esperaríamos que el comportamiento fuera que los números “1”, “2”, … “5” se imprimieran, uno a la vez, uno por segundo, respectivamente.

De hecho, si se ejecuta este código, se imprime “6” 5 veces, a intervalos de un segundo.

¿Eh?

En primer lugar, vamos a explicar de dónde viene el 6. La condición de terminación del bucle es cuando i no es <=5. La primera vez que es así es cuando tengo i es 6. Por lo tanto, la salida está reflejando el valor final de i después de que el bucle termine.

Esto realmente parece obvio en un segundo vistazo. Las llamadas de retorno de la función de tiempo de espera están funcionando bien después de la finalización del bucle. De hecho, a medida que avanzan los temporizadores, incluso si fuese setTimeout(..., 0) en cada iteración, todas esas llamadas de retorno de función se ejecutarían estrictamente después de completar el bucle, y por lo tanto imprimirían 6 cada vez.

Pero hay una cuestión más profunda en juego aquí. ¿Qué falta en nuestro código para que realmente se comporte como lo hemos implicado semánticamente?

Lo que falta es que estamos intentando implicar que cada iteración del bucle “captura” su propia copia de i, en el momento de la iteración. Pero, de la forma en que funciona el alcance, las 5 funciones, aunque se definen por separado en cada iteración del bucle, todas están cerradas sobre el mismo alcance global compartido, que tiene, de hecho, sólo un ien él.

Dicho de esa manera, por supuesto todas las funciones comparten una referencia a la misma i. Algo acerca de la estructura del bucle tiende a confundirnos al pensar que hay algo más sofisticado en el trabajo. No la hay. No habría diferencia si cada una de las 5 llamadas de timeout fueran declaradas una tras otra, sin ningún bucle.

Bien, volvamos a nuestra pregunta candente. ¿Qué falta? Necesitamos un alcance más “cerrado”. Específicamente, necesitamos un nuevo ámbito de cierre para cada iteración del bucle.

Aprendimos en el Capítulo 3 que la IIFE crea alcance declarando una función e inmediatamente
ejecutándolo.

Probémoslo:

¿Funciona eso? Inténtalo. De nuevo, esperaré.

Treminarré con el suspense NO. Pero, ¿por qué? Ahora tenemos obviamente mas alcance lexico. Cada llamada de retorno de la función setTimeout se está cerrando sobre su propio alcance por iteración creado respectivamente por cada IIFE.

No es suficiente tener un alcance para cerrarlo si está vacío. Mira atentamente. Nuestra IIFE es sólo un ámbito vacío para no hacer nada. Necesita algo en ella para sernos útil.

Necesita su propia variable, con una copia del valor de i en cada iteración.

Eureka! Funciona!

Otra variación si la prefiere:

Por supuesto, ya que estas IIFEs son sólo funciones, podemos pasarle i, y podemos llamarla j si lo preferimos, o incluso podemos llamarla i de nuevo. De cualquier manera, el código funciona ahora.

El uso de un IIFE dentro de cada iteración creó un nuevo alcance para cada iteración, lo que nos dio la oportunidad de cerrar un nuevo alcance para cada iteración, uno que tenía una variable con el valor correcto por iteración para que pudiéramos acceder a ella.

Problema resuelto!

Revisión del alcance del bloque

Observe atentamente nuestro análisis de la solución anterior. Utilizamos un IIFE para crear un nuevo alcance por iteración. En otras palabras, en realidad necesitábamos un ámbito de aplicación por bloque de iteración. El capítulo 3 nos mostró la declaración let, que roba un bloque y declara una variable allí mismo en el bloque.

Esencialmente convierte un bloque en un ámbito que podemos cerrar. Por lo tanto, el siguiente código impresionante “simplemente funciona”:

Pero eso no es todo! (con mi mejor voz de Bob Barker). Hay un comportamiento especial definido para las declaraciones let usadas en la cabeza de un bucle for. Este comportamiento dice que la variable será declarada no sólo una vez para el bucle, sino para cada iteración. Y, de forma útil, se inicializará en cada iteración subsiguiente con el valor del final de la iteración anterior.

¿Qué tan genial es eso? El alcance y el cierre de los bloques trabajan codo con codo, resolviendo todos los problemas del mundo. No sé ustedes, pero eso me hace un feliz JavaScripter.

Módulos

Hay otros patrones de código que aprovechan el poder del cierre pero que en la superficie no parecen estar relacionados con la devolución de llamadas. Examinemos el más poderoso de ellos: el módulo.

Tal y como está este código ahora mismo, no hay ningún cierre observable. Simplemente tenemos algunas variables de datos privados something y another, y un par de funciones internas doShething() y doAnother(), las cuales tienen un alcance léxico (y por lo tanto ¡cierre!) sobre el alcance interno de foo().

Pero ahora considera:

Este es el patrón en JavaScript que llamamos módulo. La forma más común de implementar el patrón de módulos es a menudo llamada “Módulo Revelador“, y es la variación que presentamos aquí.

Examinemos algunas cosas sobre este código.

En primer lugar, CoolModule() es sólo una función, pero tiene que ser invocada para que se cree una instancia de módulo. Sin la ejecución de la función externa, la creación del alcance interno y los cierres no ocurrirían.

En segundo lugar, la función CoolModule() devuelve un objeto, denotado por la sintaxis objeto-literal { clave: valor, .... }. El objeto que devolvemos tiene referencias a nuestras funciones internas, pero no a nuestras variables de datos internas. Los mantenemos ocultos y privados. Es apropiado pensar en este valor de retorno de objeto como esencialmente una API pública para nuestro módulo.

Este valor de retorno de objeto se asigna finalmente a la variable externa foo, y entonces podemos acceder a esos métodos de propiedad en la API, como foo.doSomething().

Nota: No es necesario que devolvamos un objeto (literal) de nuestro módulo. Podríamos devolver una función interna directamente. jQuery es en realidad un buen ejemplo de esto. Los identificadores jQuery y $ son la API pública para el “módulo” jQuery, pero son, en sí mismos, sólo una función (que puede tener propiedades, ya que todas las funciones son objetos).

Las funciones doShething() y doAnother() tienen cierre sobre el ámbito interno de la “instancia” del módulo (al que se llega invocando en realidad a CoolModule()). Cuando transportamos a las funciones fuera del ámbito léxico, a través de referencias de propiedad sobre el objeto que devolvemos, establecemos una condición para que el cierre pueda ser observado y ejercido.

Para decirlo de forma más sencilla, existen dos “requisitos” para usar el patrón de módulos:

  1. Debe haber una función de envolvente exterior, y debe ser invocada al menos una vez (cada vez que se hace se crea una nueva instancia de módulo).
  2. La función envolvente debe devolver al menos una función interna, de modo que esta función interna tenga cierre sobre el ámbito privado, y pueda acceder y/o modificar ese estado privado.

Un objeto con una propiedad de función no es realmente un módulo. Un objeto que se devuelve desde una invocación de función que sólo tiene propiedades de datos y ninguna función “cerrada” no es realmente un módulo, en el sentido observable.

El fragmento de código anterior muestra un creador de módulo autónomo llamado CoolModule() que puede ser invocado un número ilimitado de veces, cada vez creando una nueva instancia de módulo. Una ligera variación en este patrón es cuando sólo te importa tener un caso, una especie de “singleton“:

Aquí, convertimos nuestra función de módulo en un IIFE (ver Capítulo 3), e inmediatamente lo invocamos y asignamos su valor de retorno directamente a nuestro identificador de instancia de módulo único foo.

Los módulos son sólo funciones, por lo que pueden recibir parámetros:

Otra ligera pero poderosa variación sobre el patrón del módulo es nombrar el objeto que está devolviendo como su API pública:

Reteniendo una referencia interna al objeto publicAPI dentro de su instancia de módulo, puede modificar esa instancia de módulo desde dentro, incluyendo añadir y eliminar métodos, propiedades y cambiar sus valores.

Módulos Modernos

Varios cargadores/administradores de dependencias de módulos envuelven esencialmente este patrón de definición de módulos en una API amigable. En lugar de examinar una biblioteca en particular, permítanme presentar una prueba de concepto muy simple solamente con fines ilustrativos:

La parte clave de este código son los modules[name] = impl.apply(impl, deps). Esto invoca la función envolvente de define para un módulo (pasada como dependencia), y almacena el valor de retorno, la API del módulo, en una lista interna de módulos rastreados por nombre.

Y así es como podría usarlo para definir algunos módulos:

Nota: El método apply() invoca una determinada función asignando explícitamente el objeto this y un array o similar (array like object) como parámetros (argumentos) para dicha función.

Tanto el módulo “foo” como el módulo “bar” están definidos con una función que devuelve una API pública. “foo” incluso recibe la instancia de “bar” como parámetro de dependencia, y puede utilizarla en consecuencia.

Dedique algún tiempo a examinar estos fragmentos de código para comprender plenamente el poder de los cierres para nuestros propios propósitos. La clave es que no hay realmente ninguna “magia” particular para los manejadores de módulos. Cumplen ambas características de la estructura modular enumeradas anteriormente: invocando una envoltura de definición de función, y mantener su valor de retorno como la API para ese módulo.

En otras palabras, los módulos son sólo módulos, incluso si usted pone una herramienta de envoltura amigable encima de de ellos.

Módulos Futuros

ES6 añade soporte de sintaxis de primera clase para el concepto de módulos. Cuando se carga a través del módulo, ES6 trata un archivo como un módulo separado. Cada módulo puede importar otros módulos o miembros específicos de la API, así como exportar sus propios miembros públicos de la API.

Nota: Los módulos basados en funciones no son un patrón reconocido estáticamente (algo que el compilador conoce), por lo que su semántica de API no se considera hasta el tiempo de ejecución. Es decir, puede modificar la API de un módulo durante el tiempo de ejecución (véase lo discutido anteriormente sobre publicAPI).

Por el contrario, las APIs de módulos de ES6 son estáticas (las APIs no cambian en tiempo de ejecución). Dado que el compilador lo sabe, puede (¡y lo hace!) comprobar durante la carga de ficheros y la compilación, cada un miembro de la API de un módulo importado. Si la referencia API no existe, el compilador arroja un error “temprano” en tiempo de compilación, en lugar de esperar por resolución dinámica de tiempo de ejecución tradicional (y errores, si los hubiera).

Los módulos ES6 no tienen un formato “en línea”, deben estar definidos en ficheros separados (uno por módulo). Los navegadores/motores tienen un “cargador de módulos” por defecto (el cual se puede anular, pero que está más allá de nuestra discusión aquí) que carga sincrónicamente un archivo de módulo cuando es importado.

Considere:

bar.js

foo.js

Nota: Los archivos separados “foo.js” y “bar.js” tendrían que ser creados, con el contenido como se muestra en los dos primeros fragmentos, respectivamente. Luego, su programa cargaría/importaría esos módulos para usarlos, como se muestra en el tercer fragmento.

import importa uno o más miembros de la API de un módulo al alcance actual, cada uno a una variable que la envuelve ( hello en nuestro caso). module importa una API de módulo completa a una variable envolvente (foo , bar en nuestro caso). export exporta un identificador (variable, función) a la API pública para el módulo actual. Estos operadores pueden ser utilizados tantas veces como sea necesario en un definición del módulo según sea necesario.

Los contenidos dentro del archivo de módulo se tratan como si estuvieran encerrados en un cierre de alcance, al igual que con los módulos de cierre de funciones vistos anteriormente.

Revisión

El cierre parece a los no iluminados como un mundo místico separado dentro de JavaScript que sólo las pocas almas más valientes pueden alcanzar. Pero en realidad es sólo un estándar, y casi un hecho obvio, de cómo escribimos código en un entorno de alcance léxico que y puede ser transmitido a voluntad.

El cierre es cuando una función puede recordar y acceder a su ámbito léxico incluso cuando es invocado fuera de su ámbito léxico.

Los cierres pueden hacernos tropezar, por ejemplo con bucles, si no tenemos cuidado de reconocerlos y cómo funcionan. Pero también son una herramienta inmensamente poderosa, que permite patrones como módulos en sus diversas formas.

Los módulos requieren dos características clave:

  1. una función de envoltura externa que se invoca
  2. el valor de retorno de la función de envoltura que debe incluir referencia a por lo menos una función interna que entonces tiene cierre sobre el alcance interno privado de la envoltura.

Ahora podemos ver los cierres alrededor de nuestro código existente, y tenemos la habilidad de reconocer y aprovecharlos para nuestro propio beneficio!

6. Alcance dinámico

En el capítulo 2, hablamos de “Dynamic Scope” como un contraste con el modelo “Lexical Scope”, que es cómo funciona el alcance en JavaScript (y de hecho, la mayoría de los otros lenguajes).

Examinaremos brevemente el ámbito dinámico, para establecer el contraste. Pero, lo que es más importante, el alcance dinámico es en realidad un primo cercano de otro mecanismo (this) en JavaScript, que cubrimos en el título “this & Object Prototypes” de esta serie de libros.

Como vimos en el Capítulo 2, el alcance léxico es el conjunto de reglas acerca de cómo el Motor puede buscar una variable y dónde la encontrará. La característica clave del ámbito léxico es que se define en tiempo de autor, cuando el código está escrito (asumiendo que no haces trampas con eval() o with).

El alcance dinámico parece implicar, y por una buena razón, que hay un modelo en el que el alcance puede determinarse dinámicamente en tiempo de ejecución, en lugar de estáticamente en tiempo de autor. De hecho, este es el caso. Vamos a ilustrarlo con código:

El alcance léxico sostiene que la referencia RHS a a en foo() será resuelta a la variable global a , lo que resultará en la salida del valor 2.

El ámbito dinámico, por el contrario, no se ocupa de cómo y dónde se declaran las funciones y los ámbitos, sino más bien de desde dónde se llaman. En otras palabras, la cadena del alcance se basa en la pila de llamadas, no en el anidamiento de los alcances en el código.

Así, si JavaScript tuviese un alcance dinámico, cuando se ejecuta foo(), teóricamente el código de aquí abajo resultaría en 3 como salida.

¿Cómo puede ser esto? Porque cuando foo() no puede resolver la referencia de la variable para a , en lugar de subir por la cadena del alcance anidado (léxico), sube por la pila de llamadas, para encontrar desde dónde se llamó a foo(). Ya que foo() fue llamado desde bar(), comprueba las variables en alcance para bar(), y encuentra una a allí con valor 3.

¿Extraño? Probablemente estés pensando eso, en este momento. Pero eso es sólo porque probablemente sólo has trabajado en código que tiene un alcance léxico. Así que el alcance dinámico parece extraño. Si sólo hubieras escrito código en un lenguaje con un alcance dinámico, parecería natural, y el alcance léxico sería el extraño.

Para ser claros, JavaScript no tiene, de hecho, un alcance dinámico. Tiene un alcance léxico. Simple y llanamente. Pero este mecanismo es algo así como el alcance dinámico.

El contraste clave: el ámbito léxico es el tiempo de escritura, mientras que el ámbito dinámico (¡y this!) es el tiempo de ejecución. El ámbito léxico se preocupa de dónde se declaró una función, pero el ámbito dinámico se preocupa de dónde se llamó a una función.

Por último: this tiene en cuenta cómo se llamó una función, lo que muestra cuán estrechamente relacionado el mecanismo this con la idea de un alcance dinámico. Para profundizar más en este tema, lea el título “this & Object Prototypes”.

7. Polifyll del Alcance del bloque

En el Capítulo 3, exploramos Block Scope. Vimos que tanto with como con la cláusula catch son ejemplos diminutos del alcance de bloque que han existido en JavaScript desde al menos la introducción de ES3.

Pero es en la introducción de ES6 de let lo que finalmente le da a nuestro código una capacidad completa y sin restricciones de alcance de bloque. Hay muchas cosas emocionantes, tanto funcional como estilísticamente, que el alcance de bloque permitirá.

Pero, ¿qué pasaría si quisiéramos usar el alcance de bloque en entornos pre-ES6?

Considere este código:

Esto funcionará muy bien en entornos ES6. Pero, ¿podemos hacerlo antes de la EES6? catch es la respuesta.

Whoa! Es un código feo y raro. Vemos un try/catch que parece lanzar por la fuerza un error, pero el “error” que lanza es sólo un valor 2, y luego la declaración de la variable que la recibe está en el catch(a).

Así es, la cláusula de captura tiene un alcance de bloque, lo que significa que puede utilizarse como un polyfill (hacer que se comporte correctamente) para el alcance de bloque en entornos pre-ES6.

“Pero que dices. “…nadie quiere escribir un código tan feo como ese!” Eso es verdad. Nadie escribe (parte de) la salida de código del compilador. Ese no es el punto.

El punto es que las herramientas pueden transpilar código ES6 para trabajar en entornos pre-ES6. Usted puede escribir código usando el alcance de bloque, y beneficiarse de tal funcionalidad, y dejar que una herramienta se encargue de producir código que realmente funcionará cuando se despliegue.

Esta es en realidad la ruta de migración preferida para todos (ejem, la mayoría) a ES6: usar un código para tomar el código ES6 y producir código compatible con ES5 durante la transición de pre-ES6 a ES6.

Traceur

Google mantiene un proyecto llamado “Traceur”, que tiene exactamente la tarea de traducir las características ES6 al pre-ES6 (en su mayoría ES5, pero no todas!) para uso general. El comité del TC39 confía en esta herramienta (y otras) para probar la semántica de las características que especifican.

¿Qué produce Traceur de nuestro fragmento? ¡Lo adivinaste!

Por lo tanto, con el uso de estas herramientas, podemos empezar a aprovechar el alcance del bloque independientemente de si usamos a ES6 o no, porque el try/catch ha existido (y ha funcionado de esta manera) a partir de los días de ES3.

Implícito vs. Bloques explícitos

En el Capítulo 3, identificamos algunas trampas potenciales para la mantenibilidad/refactorabilidad del código cuando introducimos el alcance de bloque. ¿Existe otra forma de aprovechar el alcance de bloque, pero reduciendo este inconveniente?

Considere esta forma alternativa de let, llamada “bloque let” o “sentencia let” (contrastada con las “declaraciones let” de antes).

En lugar de secuestrar implícitamente un bloque existente, la sentencia let crea un bloque explícito para su alcance vinculante. No sólo se destaca más el bloque explícito, y tal vez se destaque más en la refactorización de código, sino que produce un código algo más limpio al forzar, gramaticalmente, todas las declaraciones en la parte superior del bloque. Esto hace que sea más fácil mirar cualquier bloque y saber qué es lo que abarca y qué no.

Como patrón, refleja el enfoque que muchas personas adoptan en el alcance de la función cuando mueven/elevan manualmente todas sus declaraciones var a la parte superior de la función. La sentencia let los coloca en la parte superior del bloque intencionadamente, y si no usas declaraciones de let esparcidas por todas partes, tus declaraciones de alcance del bloque son algo más fáciles de identificar y mantener.

Pero, hay un problema. La forma de declaración de “bloque” let no está incluido en ES6. Tampoco el compilador oficial de Traceur acepta esa forma de código.

Tenemos dos opciones. Podemos formatear usando sintaxis válida ES6 y un poco de disciplina en el código:

Pero, las herramientas son para resolver nuestros problemas. Por lo tanto, la otra opción es escribir bloques de sentencia explícitos y dejar que una herramienta los convierta en código de trabajo válido.

Construí una herramienta llamada “let-er” para tratar este tema. let-er es un transpondedor de código, pero su única tarea es encontrar formas de sentencias let y transpilarlos. Dejará en paz el resto de su código, incluyendo cualquier declaración let. Usted puede utilizar con seguridad let-er como primer paso del transpondedor ES6, y luego pase su código a través de algo como Traceur si es necesario.

Además, let-er tiene un indicador de configuración –es6 que al activarse (desactivado de forma predeterminada), cambia el tipo de código producido. En lugar de realizar un ES3 polyfill hack try/catch , let-er lo que hace es tomar nuestro fragmento y producir uno totalmente compatible con ES6 sin hack:

Por lo tanto, puede empezar a usar let-er inmediatamente, y apuntar a todos los entornos pre-ES6, y cuando sólo se tenga que preocupar por ES6, puede añadir el “flag” y apuntar instantáneamente sólo a ES6.

Y lo más importante, puede utilizar la forma de let preferida y más explícita aunque no sea una parte oficial de ninguna versión ES (todavía).

Rendimiento

Permítanme añadir una última nota rápida sobre la actuación de tey/catch y preguntarle, ¿por qué no usar una IIFE para crear el alcance?.

En primer lugar, la realización de try/catch es más lenta, pero no hay ninguna suposición razonable de que tiene que ser así, o incluso que siempre será así. Desde que el TC39 oficial aprobó usos del transpiler ES6 try/catch el equipo de Traceur ha pedido a Chrome que mejore la realización de try/catch y obviamente están motivados para hacerlo.

En segundo lugar, IIFE no es una comparación justa con try/catch porque una función envuelta alrededor de cualquier código arbitrario cambia el significado, dentro de ese código, de this, return, break y continue. IIFE no es un sustituto general adecuado. Sólo podía ser utilizado manualmente en ciertos casos.

La pregunta es: ¿quiere alcance de bloque o no? Si lo quiere, estas herramientas pueden darte esa opción. Si no, siga usando var y siga con su codificación!

this Léxico

Aunque este capítulo no trata el mecanismo this en detalle, hay un tema de ES6 que lo relaciona con el alcance léxico de una manera importante, que examinaremos rápidamente.

ES6 añade una forma sintáctica especial de declaración de función llamada “función flecha”. Se parece a esto:

La llamada “flecha gorda” se menciona a menudo como una abreviatura para la palabra clave tediosamente repetitiva (sarcasmo…) function.

Pero hay algo mucho más importante con las funciones flecha que no tiene nada que ver con reducir el número las pulsaciones de teclado en su declaración.

Brevemente, este código sufre un problema:

El problema es la pérdida de this en la vinculación en la función cool(). Hay varias maneras de abordar ese problema, pero una solución que se repite a menudo es var self = this;.

Se parecería a:

Sin irse mucho por las ramas, la solución var self = this sólo prescinde de todo el problema de comprender y utilizar correctamente la vinculanción de this, y en vez de eso vuelve a algo con lo que quizás nos sintamos más cómodos: el alcance léxico. self se convierte en un mero identificador que puede ser resuelto a través del alcance y cierre léxico, y no le importa lo que le pasa a la vinculación de this a lo largo del camino.

A la gente no le gusta escribir cosas repetitivas, especialmente cuando lo hacen una y otra vez. Así que, una motivación de ES6 es ayudar a aliviar estos escenarios, y de hecho, solucionar problemas comunes del lenguaje, como éste.

La solución ES6, la función-flecha, introduce un comportamiento llamado “this léxico”.

La explicación breve es que las funciones flecha no se comportan en absoluto como las funciones normales cuando se trata de la vinculación this. Estas descartan todas las reglas normales para esta vinculación, y en su lugar asumen el valor de this de su ámbito léxico inmediato, sea cual sea.

Así que, en ese fragmento, la función flecha no obtiene el valor de this de alguna manera impredecible, sólo “hereda” this de la vinculación de la función cool() (¡que es correcto si lo invocamos como se muestra!).

Aunque esto hace que el código sea más corto, mi perspectiva es que las funciones flecha están insertando en la sintaxis del lenguaje un error común de los desarrolladores, que es confundir “estas reglas vinculantes” con reglas de “alcance léxico”.

Dicho de otra manera: ¿por qué usar la molesta y repetitiva forma de paradigma de codificación de este estilo, sólo para acortarlo mezclándolo con referencias léxicas? Parece natural adoptar un enfoque u otro para cualquier pieza de código dada, y no mezclarlos en la misma pieza de código.

Nota: otra desventaja de las funciones flecha es que son anónimas, no nombradas. Vea el Capítulo 3 para las razones por las que las funciones anónimas son menos deseables que las funciones nombradas.

Un enfoque más apropiado, desde mi punto de vista, a este “problema”, es utilizar y abrazar los mecanismos the this correctamente.

Ya sea que prefiera el nuevo comportamiento del this léxico de las funciones flecha, o que prefiera el probado y verdadero bind(), es importante notar que las funciones flecha no son sólo para no escribir “function”.

Tienen una diferencia de comportamiento intencional que debemos aprender y entender, y si así lo decidimos, aprovecharla.

Ahora que entendemos completamente el alcance léxico (¡y el cierre!), entender el this léxico debería ser una brisa de aire fresco!

Aquí os dejo el en lace para la descarga de esta traducción you_dont_know_js_2_scope_and_closures


Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *