Alcance de Grokking en JavaScript
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
letyconstcambiar 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:


JavaScript ES6Fundamentos de JavaScript ES6Dan Wellman

JavaScriptTécnicas de Refactorización de JavaScriptPavan Podila
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:
- Crear una función. Las variables declaradas dentro de las funciones son visibles sólo dentro de esa función, incluso en funciones anidadas.
- Declare las variables con
letoconstdentro de un bloque de código. Estas declaraciones sólo son visibles dentro del bloque. - 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.
- En primer lugar, su fuente se compila.
- A continuación, se ejecuta el código compilado.
Durante el paso de compilación, el motor de JavaScript:
- Toma nota de todos los nombres de sus variables
- Los registra en el ámbito apropiado
- 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:
- Hay una variable denominada
first_nameen el ámbito global. - Hay
una función llamada
popupen el ámbito global. - Hay una variable
llamada
last_nameen el ámbito depopup. - Los valores de
first_nameylast_nameno están definidosundefined.
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:
- Busca el valor de
popup - Busca el valor de
first_name - Ejecuta
popupcomo una función, pasando el valor defirst_namecomo un parámetro
Cuando se ejecuta popup, pasa por este mismo
proceso, pero esta vez dentro de la función popup. Eso:
- Busca la
variable llamada
last_name - Establece el valor de
last_nameigual a"Sengstacke" - 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:
-
Puede referirse al
fooantes de asignarlo, pero su valor es indefinidoundefined. -
Puede llamar a
brokenantes de definirlo, pero obtendrá unTypeError. -
Puede llamar a la
barantes 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:
- Registra el nombre
fooen el ámbito más cercano - Establece el valor
de
fooen 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:
- 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
undefinedhasta la asignación. - 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:
-
Debe
asignar un valor al declarar con
const. -
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
letoconstantes de la asignación, se lanza unReferenceError, 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
varestán disponibles en todos los ámbitos en los que están definidos, pero tienen el valorundefinedhasta 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!



