Aprende leyendo en orden

Ámbito y hoisting — dónde y desde cuándo se puede leer una variable

El ámbito decide desde dónde se lee una variable, y el hoisting registra las declaraciones antes de ejecutar el código. Incluye shadowing, la TDZ y por qué evitar var.

Si usas fuera de unas llaves un valor que calculaste dentro de ellas, la ejecución se detiene. También se detiene cuando una línea lee un valor de configuración que se declara más abajo. En los dos casos, el problema es desde dónde y desde cuándo se puede usar un nombre.

Este artículo trata el ámbito, que decide desde dónde se puede leer un nombre declarado, y el hoisting, que registra las declaraciones antes de que se ejecute el código.

Variables que solo existen dentro de las llaves — ámbito de bloque

Supón que quieres mostrar en la pantalla del carrito cuánto le falta al cliente para tener envío gratis. Puedes calcular lo que falta dentro del if que comprueba el envío gratis, pero si intentas mostrar ese valor fuera de las llaves, la ejecución se detiene con un ReferenceError.

El ámbito (scope: la zona que decide desde dónde se puede leer un nombre) lo delimitan las llaves. La zona de un par de llaves se llama ámbito de bloque, y una variable declarada con const o let solo se puede leer dentro del ámbito de bloque más interno que rodea su declaración.

const cartTotal = 4800;            // Nivel más externo, fuera de cualquier llave

function checkShipping(total) {
  const freeLine = 5000;           // Solo dentro de las llaves de checkShipping

  if (total < freeLine) {
    const shortage = freeLine - total;  // Solo dentro de las llaves del if
    console.log(`Faltan ${shortage} yenes`);  // Faltan 200 yenes
  }

  // Las variables declaradas afuera se pueden leer desde adentro
  console.log(cartTotal);          // 4800
  // console.log(shortage);        // ReferenceError: shortage is not defined
}

checkShipping(cartTotal);
Qué variables se pueden leer desde afuera
Nivel más externo (fuera de cualquier llave)
  • const cartTotal = 4800 — se puede leer desde cualquier parte del archivo
  • checkShipping(cartTotal) — llama a la función desde el nivel más externo
Llaves de checkShipping
  • const freeLine = 5000 — solo se puede leer dentro de estas llaves
  • El cartTotal de afuera también se puede leer aquí
  • console.log(shortage) — fuera del if ya no se puede leer, así que da ReferenceError
Llaves del if
  • const shortage = 200 — solo se puede leer dentro de estas llaves
  • Se pueden leer tanto freeLine como cartTotal
El código de adentro puede leer valores de afuera, pero un valor creado dentro del if ya no se puede leer al salir de sus llaves. La lectura va en una sola dirección: de adentro hacia afuera.

El nivel más externo, fuera de cualquier llave, es el ámbito global, y sus variables se pueden leer desde cualquier parte. El cuerpo de una función también va entre llaves, así que no puedes leer freeLine desde fuera de la función. La zona que delimitan las llaves de una función se llama ámbito de función.

Muestra a los miembros cuántos puntos les faltan para el siguiente nivel. points y nextRankLine ya están declarados.

① Declara con let la variable remaining, que guardará los puntos que faltan, en un lugar donde se pueda leer también desde fuera del if.

② Solo cuando no alcancen los puntos, asigna a remaining los puntos que faltan dentro de las llaves del if.

③ Fuera del if, muéstralo con el formato "Faltan N puntos para el siguiente nivel".

(Si se ejecuta correctamente, aparecerá una explicación.)

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

Gana la declaración de adentro — shadowing

Al agregar una función que calcula precios con una tasa de impuesto reducida, puede que declares dentro de ella una variable llamada taxRate, con el mismo nombre que la de afuera. Si no sabes cuál de los dos valores se usa, no vas a poder averiguar por qué un resultado salió mal.

Si en un ámbito interno declaras un nombre que ya existe en uno externo, dentro de esas llaves se usa la declaración de adentro. Esto se llama shadowing (sombreado: una declaración interna oculta una variable externa con el mismo nombre), y pasa tanto con las llaves de un if como con las de una función.

const taxRate = 0.1;                 // Tasa de impuesto definida afuera

function withTax(price) {
  return Math.floor(price * (1 + taxRate));
}

function withReducedTax(price) {
  const taxRate = 0.08;              // Mismo nombre, declarado dentro de la función
  return Math.floor(price * (1 + taxRate));
}

console.log(withTax(1000));          // 1100
console.log(withReducedTax(1000));   // 1080
console.log(taxRate);                // 0.1 (la de afuera no cambió)
Mismo nombre, distinto valor según dónde se lea
const taxRate= 0.1 (afuera)Se lee dentrode withTaxSe lee dentro dewithReducedTaxSe lee fuera delas funcionesSin declaracióninterna: 0.1Encuentra el0.08 internoUsa el 0.1de afueraDevuelve 1100Devuelve 1080Muestra 0.1
El mismo nombre taxRate encuentra una declaración distinta según dónde lo leas. Una declaración interna no sobrescribe el valor de afuera.

Un nombre se busca empezando por las llaves más internas que rodean la línea que lo lee. Dentro de withReducedTax, la búsqueda termina en cuanto encuentra 0.08, así que el 0.1 de afuera nunca se lee. Cuesta seguir un código en el que un mismo nombre vale algo distinto en cada función, así que en tu propio código usa nombres diferentes, como reducedTaxRate.

Sin const, asignas a la variable de afuera

Si dentro de una función escribes taxRate = 0.08; sin const, no se crea ninguna variable nueva: es una asignación al taxRate de afuera. Si la de afuera es const, la ejecución se detiene con un TypeError, pero si es let, el valor se sobrescribe, y hasta una llamada posterior a withTax(1000) devuelve 1080.

Cambia el título del precio en la página de un producto, pero solo durante una oferta. label e isSale ya están declarados.

① Cuando isSale sea true, declara label como "Precio de oferta" dentro de las llaves del if y muéstralo.

② Muestra label fuera del if.

③ Escribe un segundo if en el que el nombre de adentro sea saleLabel, y muéstralo con el formato "Precio normal → Precio de oferta".

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

No se puede leer antes de la línea de declaración — hoisting y la TDZ

Supón que quieres agrupar valores de configuración, como la tasa de puntos o el costo de envío, en líneas debajo del código que los usa. Como viste en el artículo de funciones, una declaración function se puede llamar aunque esté escrita más abajo, pero si lees un valor de configuración declarado con let o const antes de su línea de declaración, la ejecución se detiene con un ReferenceError.

Antes de empezar a ejecutar un ámbito, JavaScript registra los nombres de las declaraciones que hay en él. Esto se llama hoisting (elevación: las declaraciones se registran antes de que se ejecute el código). Una declaración function registra su nombre y su cuerpo juntos, así que puedes llamarla desde líneas que están arriba de la declaración. let y const registran solo el nombre; el valor se guarda cuando se ejecuta la línea de declaración.

// El cuerpo de la función lee pointRate cuando se la llama
function calcPoint(amount) {
  return Math.floor(amount * pointRate);
}

// Si la llamas aquí, la línea de declaración de pointRate todavía no se ejecutó
// console.log(calcPoint(3200));
// → ReferenceError: Cannot access 'pointRate' before initialization

let pointRate = 0.05;              // Configuración agrupada más abajo

console.log(calcPoint(3200));      // 160 (se puede leer porque la línea de declaración ya se ejecutó)
Cuándo se puede leer el valor de un let
El ámbito empiezaa ejecutarseRegistra solo elnombre pointRateHasta llegar ala línea del letLlamar a calcPoint→ ReferenceErrorlet pointRate= 0.05 se ejecutaDesde aquí lasllamadas dan 160
Hasta que se ejecuta la línea del let, el valor todavía no existe, aunque lo lea una función escrita más arriba. Que se pueda leer o no depende del orden de ejecución, no de dónde lo escribiste.

El tramo que va desde que se entra en un ámbito hasta que se ejecuta la línea de declaración del let se llama TDZ (Temporal Dead Zone, zona muerta temporal: el periodo en el que un nombre no se puede leer hasta que se ejecuta su línea de declaración). Si el mensaje de error dice is not defined, el nombre no está en el ámbito. Si dice before initialization, el nombre existe, pero lo leíste antes de que se ejecutara su línea de declaración.

Muestra en la pantalla de confirmación del pedido cuánto falta para el envío gratis y los puntos ganados. orderTotal ya está declarado.

① Declara con let el umbral de envío gratis freeLine con el valor 5000 y, en la línea de abajo, muestra "Faltan N yenes para el envío gratis".

② Escribe primero la línea que muestra "Puntos ganados: N" y, en la línea de abajo, declara con let la tasa de puntos pointRate con el valor 0.05.

③ Comprueba cuántas líneas se muestran y con qué error se detiene la ejecución.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

Se registra antes con undefined — por qué no usar var

En artículos y librerías antiguos vas a ver variables declaradas con var, y si copias código que funciona, puede que var se cuele con él. Pero var se diferencia de const y let en dos cosas: dónde es válida la variable y qué obtienes si la lees antes de su declaración.

Una declaración var también se registra antes, pero al mismo tiempo se le guarda undefined. Por eso, si la referencias en una línea que está arriba de la declaración, no hay error: la ejecución sigue adelante con undefined. Además, no se limita a las llaves: es válida en toda la función que la contiene.

function checkStock(orderCount) {
  // Leerla antes de la declaración no detiene nada; contiene undefined
  console.log(stock);              // undefined

  var stock = 12;
  console.log(stock);              // 12

  if (orderCount <= stock) {
    var status = "Listo para enviar";         // Declarada con var
    let note = "Enviar desde el almacén A";   // Declarada con let
  }
  // var es válida en toda la función, así que se puede leer fuera del if
  console.log(status);             // Listo para enviar
  // console.log(note);            // ReferenceError: note is not defined
}

checkStock(3);
Las llaves no delimitan a var
var status seasigna en el ifSale de lasllaves del ifSigue dentro dela función: se leeSe muestra:Listo para enviarlet note seasigna en el ifSale de lasllaves del ifnote solo existedentro del ifSe detiene conReferenceError
Aunque la asignación ocurra dentro del mismo if, el valor de un var se puede leer en las líneas que siguen al cierre de las llaves. Con let, la ejecución se detiene en la línea que lo lee fuera de su zona.

Una función tiene una sola variable por cada nombre declarado con var, así que volver a declarar el mismo nombre con var dentro de un if no produce shadowing: sobrescribe la misma variable. La tabla de abajo muestra, para cada forma de declarar, qué se guarda cuando el nombre se registra antes de ejecutar el código y qué obtienes si lo lees en una línea que está arriba de la declaración.

DeclaraciónQué se registra antes de ejecutarSi se lee arriba de la declaración
var stock = 12El nombre y undefinedSigue adelante con undefined
let stock = 12Solo el nombre (sin valor)Se detiene con ReferenceError
const stock = 12Solo el nombre (sin valor)Se detiene con ReferenceError

Al cambiar var por let, algunas líneas se detienen

Cuando reemplazas var por let en código antiguo, las líneas que leían la variable antes de su declaración, o fuera del if, se detienen con un ReferenceError. Esas líneas en realidad estaban leyendo undefined o un valor de otras llaves. Para corregirlas, mueve la declaración arriba de la línea que la lee, dentro de unas llaves que también rodeen esa línea.

Comprueba cómo se comporta una variable var según dónde se declare. cartTotal ya está declarado.

① Escribe primero la línea que muestra "Total a pagar: N yenes", restando discount a cartTotal, y debajo declara discount con var con el valor 300.

② Debajo de eso, vuelve a mostrarlo.

③ Si el total es de 5000 yenes o más, vuelve a declarar discount con var con el valor 800 dentro de un if.

④ Fuera del if, muestra "Descuento: N yenes".

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado
QUIZ

Verificación de conocimientos

Responde cada pregunta una a una.

Pregunta 1¿Qué pasa si muestras unit en la línea que sigue a if (true) { const unit = "yen"; }?

Pregunta 2Cerca del inicio del archivo defines una función show que devuelve rate. Luego la llamas en una línea debajo de let rate = 0.1;. ¿Qué devuelve?

Pregunta 3Si escribes console.log(count); en una línea arriba de var count = 3;, ¿qué se muestra?