Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.
El post anterior sobre expresión y sentencia cerró con una promesa: explicar qué determina, en cada punto del código, si un nombre está visible o no. Esa regla se llama scope, y ya estuvo actuando en silencio en cada ejemplo de la serie: es la razón por la que una variable declarada dentro de una función nunca aparece por accidente en otra parte del código.
El scope global: lo que vive afuera de toda función
global.js
let mensaje = "hola desde afuera";function mostrar() { console.log(mensaje);}mostrar();// hola desde afuera
mensaje se declara en el nivel superior del archivo, fuera de cualquier función. Ese lugar es el scope global: cualquier función definida en el mismo archivo, sin importar cuántas líneas de distancia haya, puede leer esa variable. mostrar no recibe mensaje como parámetro ni la declara adentro: la encuentra directamente, porque el scope global queda accesible desde todos lados.
El scope de una función: lo que se declara adentro, se queda adentro
funcion.js
function calcular() { let resultado = 10 + 5; console.log(resultado);}calcular();// 15console.log(resultado);// ReferenceError: resultado is not defined
resultado se declara dentro de calcular, así que solo existe mientras esa función se ejecuta y solo es visible para el código escrito dentro de sus llaves. Apenas la ejecución sale de la función, resultado deja de existir: intentar leerla desde afuera tira el mismo tipo de error que ya apareció en el post anterior (ReferenceError), aunque acá aparece por un motivo distinto. No es que resultado nunca se haya declarado en ningún lado: es que se declaró en un scope al que la línea que intenta leerla no tiene acceso.
Cada llamada a una función crea, además, un scope de función propio y nuevo: dos llamadas distintas a calcular tendrían cada una su propio resultado, sin que una pise a la otra.
El scope de bloque: por qué convenía evitar var
Una función no es el único lugar que delimita un scope propio. Cualquier bloque (un par de llaves, sin nada más alrededor) delimita uno también, pero solo para let y const.
bloque.js
{ let a = 1; var b = 2;}console.log(b);// 2console.log(a);// ReferenceError: a is not defined
Esto es lo que quedaba pendiente de la mención a var en el post de variables y funciones. let y const respetan cada bloque donde aparecen: la variable deja de existir apenas se cierra la llave que la contiene. var, en cambio, ignora esa frontera: la única que respeta es la de la función (o la del archivo completo, si no está dentro de ninguna función). Por eso b sigue disponible después del bloque y a no, aunque las dos se hayan declarado en el mismo lugar. Es la razón concreta por la que esta serie usa siempre let o const: con var, una variable pensada para vivir solo dentro de un bloque puntual termina filtrándose a un scope mucho más amplio del que se buscaba.
Scope léxico: importa dónde se escribió la función, no desde dónde se la llama
lexico.js
function externa() { let nombre = "Fernando"; function interna() { console.log(nombre); } interna();}externa();// Fernando
interna está escrita dentro del cuerpo de externa, así que puede leer nombre aunque nombre no sea suya: la busca en el scope que la envuelve, el de externa, porque ahí es exactamente donde quedó escrita. Esa palabra, léxico, se refiere justo a eso: al lugar del código fuente donde una función queda definida, no al momento ni al lugar desde donde se la termina llamando.
no-lexico.js
function otraExterna() { let nombre = "Ana";}function otraInterna() { console.log(nombre);}otraExterna();otraInterna();// ReferenceError: nombre is not defined
Acá otraInterna no está escrita dentro de otraExterna: es una función aparte, definida al mismo nivel. Aunque se la llame después de otraExterna, y aunque las dos compartan el mismo nombre de variable en su prosa, otraInterna no tiene ningún acceso a nombre. No importa el orden en que se llaman las funciones ni si una ya terminó de ejecutarse: lo único que importa es dónde quedó escrita cada una en el archivo. Ese es el punto central del scope léxico, y la diferencia entre este ejemplo y el anterior es la única que decide si el acceso funciona o no.
Hoisting: existir antes de tiempo, pero sin valor todavía
hoisting.js
function conVar() { console.log(contador); var contador = 0; console.log(contador);}conVar();// undefined// 0
Antes de ejecutar una función línea por línea, el motor la revisa completa y reserva de antemano un lugar para cada variable declarada con var dentro de ese scope. Ese adelanto se llama hoisting: "elevar" la declaración, no la asignación, hasta el principio del scope donde ocurre. Por eso el primer console.log(contador) no tira ningún error (contador ya existe, el motor lo sabe desde el principio), pero tampoco imprime 0: la línea que le asigna ese valor todavía no se ejecutó, así que imprime undefined, el valor por defecto de una variable que existe pero no fue inicializada.
temporal.js
function conLet() { console.log(total); let total = 0;}conLet();// ReferenceError: Cannot access 'total' before initialization
let y const también reservan un lugar de antemano para su nombre, pero no dejan usarlo antes de llegar a su propia línea de declaración: el error ya no es "no existe" (el mismo mensaje de las dos secciones anteriores), es un mensaje distinto, "no se puede acceder antes de la inicialización". La diferencia entre los dos mensajes es la diferencia entre una variable que directamente no existe en ese scope y una que existe pero todavía no llegó al punto del código donde se le asigna su primer valor.
Idea central
Cada variable vive encerrada en algún scope: el global, en el nivel superior del archivo; el de una función, que se crea de nuevo en cada llamada; el de un bloque, que solo respetan let y const. La regla que decide qué puede ver cada función es léxica: depende de dónde quedó escrita en el código fuente, nunca de desde dónde ni cuándo se la termina llamando.
El próximo post trata los closures: qué pasa cuando una función se lleva consigo el scope en el que fue definida, incluso después de que ese scope debería haber terminado.