1. Code
  2. JavaScript

Alcance de Grokking en JavaScript

Scroll to top
16 min read

Spanish (Español) translation by Elías Nicolás (you can also view the original English article)

El ámbito, o el conjunto de reglas que determinan dónde viven sus variables, es uno de los conceptos más básicos de cualquier lenguaje de programación. Es tan fundamental, de hecho, que es fácil olvidar lo sutil que pueden ser las reglas.

Comprender exactamente cómo el motor de JavaScript "piensa" sobre el alcance le impedirá escribir los errores comunes que el levantamiento puede causar, prepararle para envolver su cabeza alrededor de cierres, y conseguir que mucho más cerca de nunca escribir fallos nunca más....

...Bueno, te ayudará a entender el levantamiento y los cierres, de todos modos.

En este artículo, echaremos un vistazo a:

  • Los fundamentos de los ámbitos en JavaScript
  • Cómo el intérprete decide qué variables pertenecen a qué ámbito
  • Cómo el levantamiento realmente funciona
  • Cómo las palabras clave ES6 let y const cambiar el juego

Vamos a profundizarlos.

Si está interesado en aprender más sobre ES6 y cómo aprovechar la sintaxis y las características para mejorar y simplificar su código JavaScript, ¿por qué no echa un vistazo a estos dos cursos:

Alcance léxico

Si has escrito incluso una línea de JavaScript antes, sabrás que donde defines tus variables determina dónde puedes usarlas. El hecho de que la visibilidad de una variable depende de la estructura de su código fuente se denomina ámbito léxico.

Hay tres formas de crear el ámbito en JavaScript:

  1. Crear una función. Las variables declaradas dentro de las funciones son visibles sólo dentro de esa función, incluso en funciones anidadas.    
  2. Declare las variables con let o const dentro de un bloque de código. Estas declaraciones sólo son visibles dentro del bloque.    
  3. Cree un bloque catch. Lo creas o no, esto realmente crea un nuevo alcance!
1
"use strict";
2
var mr_global = "Mr Global";
3
4
function foo () {
5
    var mrs_local = "Mrs Local";
6
    console.log("I can see " + mr_global + " and " + mrs_local + ".");
7
    
8
    function bar () {
9
        console.log("I can also see " + mr_global + " and " + mrs_local + ".");
10
    }
11
}
12
13
foo(); // Works as expected

14
15
try {
16
    console.log("But /I/ can't see " + mrs_local + "."); 
17
} catch (err) {
18
    console.log("You just got a " + err + ".");
19
}
20
21
{
22
    let foo = "foo";
23
    const bar = "bar";
24
    console.log("I can use " + foo + bar + " in its block...");
25
}
26
27
try {
28
    console.log("But not outside of it.");   
29
} catch (err) {
30
    console.log("You just got another " + err + ".");
31
}
32
33
// Throws ReferenceError!

34
console.log("Note that " + err + " doesn't exist outside of 'catch'!") 

El fragmento anterior muestra los tres mecanismos de ámbito. Puedes ejecutarlo en Node o Firefox, pero Chrome no juega bien con let, aun.

Vamos a hablar de cada uno de estos en exquisito detalle. Comencemos con un análisis detallado de cómo JavaScript calcula qué variables pertenecen a qué ámbito.

El proceso de compilación: una vista de pájaro

Cuando se ejecuta un pedazo de JavaScript, dos cosas suceden para que funcione.

  1. En primer lugar, su fuente se compila.
  2. A continuación, se ejecuta el código compilado.

Durante el paso de compilación, el motor de JavaScript:    

  1. Toma nota de todos los nombres de sus variables
  2. Los registra en el ámbito apropiado
  3. Reserva espacio para sus valores

Es sólo durante la ejecución que el motor de JavaScript realmente establece el valor de las referencias de las variables igual a sus valores de asignación. Hasta entonces, son indefinidos undefined.

Paso 1: Compilación

1
// I can use first_name anywhere in this program 

2
var first_name = "Peleke";
3
4
function popup (first_name) {
5
    // I can only use last_name inside of this function

6
    var last_name = "Sengstacke";
7
    alert(first_name + ' ' + last_name);
8
}
9
10
popup(first_name);

Vamos a paso a través de lo que hace el compilador.

Primero, lee la línea var first_name = "Peleke". A continuación, determina qué ámbito para guardar la variable. Debido a que estamos en el nivel superior de la secuencia de comandos, se da cuenta de que estamos en el ámbito global. A continuación, guarda la variable first_name en el ámbito global e inicializa su valor en undefined.

En segundo lugar, el compilador lee la línea con la function popup (first_name). Debido a que la palabra clave function es la primera cosa en la línea, crea un nuevo ámbito para la función, registra la definición de la función en el ámbito global y busca dentro para buscar declaraciones de variables.

Efectivamente, el compilador encuentra uno. Dado que tenemos var last_name = "Sengstacke" en la primera línea de nuestra función, el compilador guarda la variable last_name en el ámbito de popup-not to the global scope-y establece su valor en undefined.

Dado que no hay más declaraciones de variables dentro de la función, el compilador regresa al ámbito global. Y puesto que no hay más declaraciones de variables allí, esta fase se hace.

Tenga en cuenta que todavía no hemos ejecutado nada. El trabajo del compilador en este punto es sólo para asegurarse de que conoce el nombre de todos; No le importa lo que hagan.

En este punto, nuestro programa sabe que:

  1. Hay una variable denominada first_name en el ámbito global.
  2. Hay una función llamada popup en el ámbito global.
  3. Hay una variable llamada last_name en el ámbito de popup.   
  4. Los valores de first_name y last_name no están definidos undefined.

No importa que hayamos asignado esos valores de variables a otra parte de nuestro código. El motor se encarga de eso en ejecución.

Paso 2: Ejecución

Durante el siguiente paso, el motor lee nuestro código de nuevo, pero esta vez, lo ejecuta.

Primero, lee la línea, var first_name = "Peleke". Para ello, el motor busca la variable denominada first_name. Dado que el compilador ya ha registrado una variable con ese nombre, el motor lo encuentra y establece su valor en "Peleke".

A continuación, lee la línea, function popup (first_name). Dado que no estamos ejecutando la función aquí, el motor no está interesado y se salta sobre ella.

Finalmente, lee la línea popup(first_name). Puesto que estamos ejecutando una función aquí, el motor:

  1. Busca el valor de popup
  2. Busca el valor de first_name   
  3. Ejecuta popup como una función, pasando el valor de first_name como un parámetro

Cuando se ejecuta popup, pasa por este mismo proceso, pero esta vez dentro de la función popup. Eso:

  1. Busca la variable llamada last_name
  2. Establece el valor de last_name igual a "Sengstacke"   
  3. Busca la alerta alert, ejecutándola como una función con "Peleke Sengstacke" como su parámetro

¡Resulta que hay mucho más por debajo de la campana de lo que podríamos haber pensado!

Ahora que usted entiende cómo el Javascript lee y funciona el código que usted escribe, estamos listos para abordar algo un poco más cercano al hogar: cómo funciona el levantamiento.

Levantamiento bajo el microscopio

Empecemos con algún código.

1
bar();
2
3
function bar () {
4
    if (!foo) {
5
        alert(foo + "? This is strange...");
6
    }
7
    var foo = "bar";
8
}
9
10
broken(); // TypeError!

11
var broken = function () {
12
    alert("This alert won't show up!");
13
}

Si ejecuta este código, notará tres cosas:

  1. Puede referirse al foo antes de asignarlo, pero su valor es indefinido undefined.
  2. Puede llamar a broken antes de definirlo, pero obtendrá un TypeError.
  3. Puede llamar a la bar antes de definirla, y funciona como se desee.

La elevación Hoisting se refiere al hecho de que JavaScript hace que todos nuestros nombres de variables declarados estén disponibles en todas partes en sus ámbitos, incluso antes de asignarlos a ellos.

Los tres casos en el fragmento son los tres que necesitará tener en cuenta en su propio código, así que vamos a paso a través de cada uno de ellos uno por uno.

Declaraciones variables de Hoisting

Recuerde, cuando el compilador de JavaScript lee una línea como var foo = "bar", eso:

  1. Registra el nombre foo en el ámbito más cercano    
  2. Establece el valor de foo en indefinido

La razón por la que podemos usar foo antes de asignarle es porque, cuando el motor busca la variable con ese nombre, existe. Es por eso que no produce un ReferenceError.

En su lugar, obtiene el valor undefined, e intenta usarlo para hacer lo que pidió. Normalmente, eso es un error.

Teniendo esto en cuenta, podemos imaginar que lo que JavaScript ve en nuestra funcion bar es más parecida a esto:

1
function bar () {
2
    var foo; // undefined

3
    if (!foo) {
4
        // !undefined is true, so alert

5
        alert(foo + "? This is strange...");
6
    }
7
    foo = "bar";
8
}

Esta es la primera regla de Hoisting, si lo desea: Las variables están disponibles en todo su alcance, pero tienen el valor undefined hasta que su código les asigna.

Un lenguaje de JavaScript común es escribir todas las declaraciones var en la parte superior de su ámbito, en lugar de donde las usa por primera vez. Parafraseando a Doug Crockford, esto ayuda a que su código se lea más como se ejecuta.

Cuando piensas en ello, eso tiene sentido. Es bastante claro por qué la bar se comporta de la manera que lo hace cuando escribimos nuestro código de la forma en que JavaScript lo lee, ¿no? Entonces, ¿por qué no escribir así todo el tiempo?

Expresiones de la función Hoisting

El hecho de que tengamos un TypeError cuando intentamos ejecutar broken antes de que lo definamos es solo un caso especial de la Primera Regla de Hoisting.

Definimos una variable, llamada broken, que el compilador registra en el ámbito global y establece igual a undefined. Cuando tratamos de ejecutarlo, el motor busca el valor de broken, encuentra que es undefined, e intenta ejecutar undefined como una función.

Obviamente, undefined no es una función, ¡por eso tenemos un TypeError!

Declaraciones de la función Hoisting

Por último, recuerde que hemos podido llamar a bar antes de que lo definimos. Esto se debe a la Segunda Regla de Hoisting: Cuando el compilador JavaScript encuentra una declaración de función, hace que su nombre y definición estén disponibles en la parte superior de su ámbito. Reescribiendo nuevamente nuestro código:

1
function bar () {
2
    if (!foo) {
3
        alert(foo + "? This is strange...");
4
    }
5
    var foo = "bar";
6
}
7
8
var broken; // undefined

9
10
bar(); // bar is already defined, executes fine

11
12
broken(); // Can't execute undefined!

13
14
broken = function () {
15
    alert("This alert won't show up!");
16
}

Una vez más, tiene mucho más sentido cuando escribe como JavaScript lee, ¿no crees?

Para revisar:

  1. Los nombres de las declaraciones de variables y las expresiones de función están disponibles en todo su ámbito, pero sus valores no están definidos undefined hasta la asignación.    
  2. Los nombres y definiciones de las declaraciones de funciones están disponibles en todo su alcance, incluso antes de sus definiciones.

Ahora echemos un vistazo a dos nuevas herramientas que funcionan un poco diferente: let y const.

Let, const, & la zona muerta temporal

A diferencia de las declaraciones var, las variables declaradas con let y const no se hoist por el compilador.

Al menos, no exactamente.

¿Recuerdas cómo pudimos llamar broken, pero consiguió un TypeError porque intentamos ejecutar undefined? Si lo hubiéramos definido broken con let, habríamos conseguido un ReferenceError, en su lugar:

1
"use strict"; 
2
// You have to "use strict" to try this in Node

3
broken(); // ReferenceError!

4
let broken = function () {
5
    alert("This alert won't show up!");
6
}

Cuando el compilador JavaScript registra variables en sus ámbitos en su primer paso, trata let y const de manera diferente que var.

Cuando encuentra una declaración var, registramos el nombre de la variable en su ámbito e inmediatamente inicializamos su valor a undefined.

Con let, sin embargo, el compilador registra la variable en su ámbito, pero no inicializa su valor a undefined. En su lugar, deja la variable no inicializada, hasta que el motor ejecuta su instrucción de asignación. Al acceder al valor de una variable no inicializada se lanza un ReferenceError, que explica por qué el fragmento anterior se ejecuta cuando lo ejecutamos.

El espacio entre el comienzo de la parte superior del ámbito de una declaración let y la sentencia de asignación se denomina zona muerta temporal. El nombre proviene del hecho de que, aunque el motor conoce una variable llamada foo en la parte superior del ámbito de la bar, la variable está "muerta", porque no tiene un valor....

También porque matará tu programa si intentas usarlo temprano.

La palabra clave const funciona de la misma forma que let, con dos diferencias clave:   

  1. Debe asignar un valor al declarar con const.
  2. No puede reasignar valores a una variable declarada con const.

Esto garantiza que const tendrá siempre el valor que inicialmente le asignó.

1
// This is legal

2
const React = require('react');
3
4
// This is totally not legal

5
const crypto;
6
crypto = require('crypto');

Alcance del bloque

Let y const son diferentes de var en una otra forma: el tamaño de sus ámbitos.

Cuando declara una variable con var, es visible tan alto en la cadena de alcance como sea posible, normalmente en la parte superior de la declaración de función más cercana o en el ámbito global si lo declara en el nivel superior.

Sin embargo, cuando declara una variable con let o const, es visible lo más localmente posible, sólo dentro del bloque más cercano.

Un bloque es una sección de código desencadenada por llaves, como se ve con bloques if / else, para bucles for y en bloques de código explícitamente "bloqueados", como en este fragmento.

1
"use strict";
2
3
{
4
  let foo = "foo";
5
  if (foo) {
6
      const bar = "bar";
7
      var foobar = foo + bar;
8
9
      console.log("I can see " + bar + " in this bloc.");
10
  }
11
  
12
  try {
13
    console.log("I can see " + foo + " in this block, but not " + bar + ".");
14
  } catch (err) {
15
    console.log("You got a " + err + ".");
16
  }
17
}
18
19
try {
20
  console.log( foo + bar ); // Throws because of 'foo', but both are undefined

21
} catch (err) {
22
  console.log( "You just got a " + err + ".");
23
}
24
25
console.log( foobar ); // Works fine

Si declara una variable con const o let dentro de un bloque, sólo es visible dentro del bloque, y sólo después de haberlo asignado.

Una variable declarada con var, sin embargo, es visible lo más lejos posible, en este caso, en el ámbito global.

Si usted está interesado en los detalles de nitty-gritty de let y const, echa un vistazo a lo que el Dr. Rauschmayer tiene que decir acerca de ellos en Exploring ES6: Variables and Scoping, y eche un vistazo a la documentación MDN en ellos.

Lexical this y funciones de la flecha

En la superficie, this no parece tener mucho que ver con el alcance. Y, de hecho, JavaScript no resuelve el significado de this de acuerdo con las reglas de alcance que hemos hablado aquí.

Por lo menos, no generalmente. JavaScript, notoriamente, no resuelve el significado de esta palabra clave this en función de dónde la usaste:

1
var foo = {
2
    name: 'Foo',
3
    languages: ['Spanish', 'French', 'Italian'],
4
    speak : function speak () {
5
        this.languages.forEach(function(language) {
6
            console.log(this.name + " speaks " + language + ".");
7
        })
8
    }
9
};
10
11
foo.speak();

La mayoría de nosotros esperaría que this significara foo dentro del bucle forEach, porque eso es lo que significaba justo fuera de él. En otras palabras, esperamos que JavaScript resuelva el significado de this de forma léxica.

Pero no lo hace.

En su lugar, crea una nueva this dentro de cada función que defina y decide qué significa en función de cómo se llama a la función, no donde la definió.

Ese primer punto es similar al caso de redefinir cualquier variable en un ámbito secundario:

1
function foo () {
2
    var bar = "bar";
3
    function baz () {
4
        // Reusing variable names like this is called "shadowing" 

5
        var bar = "BAR";
6
        console.log(bar); // BAR

7
    }
8
    baz();
9
}
10
11
foo(); // BAR

Reemplazar bar con this, y ¡toda la cosa se debe aclarar al instante!

Tradicionalmente, conseguir que this funcione como esperamos que las variables viejas del ámbito léxico común funcionen requiere una de dos soluciones:

1
var foo = {
2
    name: 'Foo',
3
    languages: ['Spanish', 'French', 'Italian'],
4
    speak_self : function speak_s () {
5
        var self = this;
6
        self.languages.forEach(function(language) {
7
            console.log(self.name + " speaks " + language + ".");
8
        })
9
    },
10
    speak_bound : function speak_b () {
11
        this.languages.forEach(function(language) {
12
            console.log(this.name + " speaks " + language + ".");
13
        }.bind(foo)); // More commonly:.bind(this);

14
    }
15
};

En speak_self, guardamos el significado de this en la variable self, y usamos esa variable para obtener la referencia que queremos. En speak_bound, usamos bind para señalar permanentemente this a un objeto dado.

ES2015 nos trae una nueva alternativa: funciones de flecha.

A diferencia de las funciones "normales", las funciones de las flechas no sombrean este valor de su ámbito this padre estableciendo su propio. Más bien, resuelven su significado léxicamente.

En otras palabras, si utiliza this en una función de flecha, JavaScript busca su valor como lo haría con cualquier otra variable.

En primer lugar, comprueba el ámbito local de this valor. Dado que las funciones de flecha no establecen una, no encontrará ninguna. A continuación, comprueba el ámbito padre para this valor. Si encuentra uno, usará eso, en su lugar.

Esto nos permite reescribir el código anterior de la siguiente manera:

1
var foo = {
2
    name: 'Foo',
3
    languages: ['Spanish', 'French', 'Italian'],
4
    speak : function speak () {
5
        this.languages.forEach((language) => {
6
            console.log(this.name + " speaks " + language + ".");
7
        })
8
    }
9
};   

Si desea más detalles sobre las funciones de flechas, eche un vistazo a instructor de Envato Tuts+ Dan Wellman excelente curso sobre JavaScript ES6 Fundamentals, así como la documentación de MDN sobre las funciones de flecha.

Conclusión

¡Hemos cubierto mucho terreno hasta ahora! En este artículo, has aprendido que:

  • Las variables se registran en sus ámbitos durante la compilación y se asocian con sus valores de asignación durante la ejecución.
  • Si se hace referencia a las variables declaradas con let o const antes de la asignación, se lanza un ReferenceError, y que dichas variables se delimitan al bloque más cercano.
  • Las funciones de la flecha nos permiten alcanzar la unión lexical de this , y puentear la vinculación dinámica tradicional.

Usted también ha visto las dos reglas de hoisting:   

  • La primera regla de Hoisting: Las expresiones de función y las declaraciones var están disponibles en todos los ámbitos en los que están definidos, pero tienen el valor undefined hasta que se ejecuten las instrucciones de asignación.
  • La segunda regla de Hoisting: Que los nombres de las declaraciones de funciones y sus cuerpos están disponibles a través de los ámbitos donde están definidos.

Un buen paso siguiente es utilizar su conocimiento newfangled de los ámbitos de JavaScript para envolver su cabeza alrededor de cierres. Para eso, echa un vistazo a Scopes & Closures de Kyle Simpson.

Por último, hay mucho más que decir sobre this de lo que pude cubrir aquí. Si la palabra clave todavía parece una magia negra, eche un vistazo a this y Prototipos de Objetos para obtener su cabeza alrededor de él.

Mientras tanto, tome lo que ha aprendido y ¡vaya a escribir menos errores!