Scope, Hoisting y Closures en Profundidad
Objetivos de aprendizajeAl finalizar esta lección serás capaz de:
- Explicar los tres niveles de scope en JavaScript (global, función, bloque)
- Predecir el comportamiento del hoisting con var, let, const y funciones
- Crear y utilizar closures para encapsular datos y lógica
- Entender la Temporal Dead Zone (TDZ) y cómo evitar errores relacionados
- Aplicar IIFE (Immediately Invoked Function Expressions) cuando sea necesario
1. Scope: dónde viven las variables
El scope (alcance) determina en qué partes de tu código una variable es accesible. Pensalo como habitaciones en una casa: lo que está en el living es visible desde cualquier habitación (global), pero lo que está en tu dormitorio es privado (local).
Scope global
Las variables declaradas fuera de cualquier función o bloque tienen scope global:
const APP_NOMBRE = "Cursalo"; // Scope global
let contadorVisitas = 0; // Scope global
function mostrarInfo() {
// Puede acceder a variables globales
console.log(APP_NOMBRE); // "Cursalo"
contadorVisitas++; // Modifica la global
}
mostrarInfo();
console.log(contadorVisitas); // 1
Problema con globales: Las variables globales pueden ser modificadas desde cualquier parte del código, lo que hace difícil rastrear bugs. La regla es: usá la menor cantidad de variables globales posible.
Scope de función
Las variables declaradas dentro de una función solo existen dentro de ella:
function procesarPago(monto) {
const impuesto = monto * 0.16; // Solo existe dentro de procesarPago
const total = monto + impuesto; // Solo existe dentro de procesarPago
console.log(`Total: $${total}`);
return total;
}
procesarPago(1000); // "Total: $1160"
// console.log(impuesto); // ❌ ReferenceError: impuesto is not defined
// console.log(total); // ❌ ReferenceError: total is not defined
Scope de bloque (ES6+)
let y const respetan el scope de bloque (cualquier código entre {}):
if (true) {
let bloque1 = "Solo visible aquí";
const bloque2 = "También solo aquí";
var noBloque = "¡Visible fuera!"; // var NO respeta scope de bloque
}
// console.log(bloque1); // ❌ ReferenceError
// console.log(bloque2); // ❌ ReferenceError
console.log(noBloque); // ✅ "¡Visible fuera!" (por eso no usar var)
// El scope de bloque incluye for, while, if, switch, y cualquier {}
for (let i = 0; i < 3; i++) {
// i solo existe aquí
}
// console.log(i); // ❌ ReferenceError
// Pero con var:
for (var j = 0; j < 3; j++) {
// j existe fuera del for
}
console.log(j); // 3 (¡se escapó del bloque!)
Cadena de scope (Scope Chain)
Cuando JavaScript busca una variable, primero mira en el scope actual, luego en el scope padre, y así hasta llegar al scope global:
const global = "🌍 Global";
function externa() {
const externaVar = "📦 Externa";
function media() {
const mediaVar = "📋 Media";
function interna() {
const internaVar = "🔑 Interna";
// Puede acceder a TODO
console.log(internaVar); // ✅ "🔑 Interna"
console.log(mediaVar); // ✅ "📋 Media"
console.log(externaVar); // ✅ "📦 Externa"
console.log(global); // ✅ "🌍 Global"
}
interna();
// console.log(internaVar); // ❌ No accede hacia adentro
}
media();
}
externa();
2. Hoisting: la elevación invisible
Hoisting es el mecanismo de JavaScript que "eleva" declaraciones de variables y funciones al inicio de su scope antes de ejecutar el código. Pero no todas se elevan igual.
Hoisting de var
console.log(x); // undefined (no da error, pero no tiene valor)
var x = 5;
console.log(x); // 5
// Lo que JavaScript hace internamente:
// var x; // Declaración elevada al inicio
// console.log(x); // undefined
// x = 5; // Asignación queda en su lugar
// console.log(x); // 5
Hoisting de let y const (Temporal Dead Zone)
// console.log(y); // ❌ ReferenceError: Cannot access 'y' before initialization
let y = 10;
// La TDZ (Temporal Dead Zone) es el espacio entre el inicio del bloque
// y la declaración de la variable. Acceder a la variable en la TDZ causa error.
{
// -- Inicio de la TDZ para 'nombre' --
// console.log(nombre); // ❌ ReferenceError
// -- Fin de la TDZ --
let nombre = "Ana";
console.log(nombre); // ✅ "Ana"
}
// Esto es BUENO porque te obliga a declarar antes de usar
Hoisting de funciones
// Las declaraciones de función se elevan COMPLETAS
saludar(); // ✅ "¡Hola!"
function saludar() {
console.log("¡Hola!");
}
// Las expresiones de función NO se elevan
// despedir(); // ❌ ReferenceError (o TypeError si se usó var)
const despedir = function() {
console.log("¡Adiós!");
};
// Las arrow functions tampoco se elevan
// multiplicar(2, 3); // ❌ ReferenceError
const multiplicar = (a, b) => a * b;
Resumen de hoisting
| Declaración | ¿Se eleva? | Valor antes de declaración |
|---|---|---|
var x = 5 |
Sí (solo declaración) | undefined |
let x = 5 |
Sí (pero TDZ) | ReferenceError |
const x = 5 |
Sí (pero TDZ) | ReferenceError |
function f(){} |
Sí (completa) | Función utilizable |
const f = () => {} |
Sí (pero TDZ) | ReferenceError |
3. Closures: el superpoder de JavaScript
Un closure se crea cuando una función interna accede a variables de una función externa, incluso después de que la función externa haya terminado de ejecutarse.
¿Por qué son útiles?
Los closures permiten:
- Encapsular datos (crear variables "privadas")
- Mantener estado entre llamadas
- Crear funciones fábrica personalizadas
- Implementar patrones como módulos, memoización, currying
Patrón Módulo (Module Pattern)
function crearCarrito() {
// Variables privadas — no se pueden acceder desde fuera
let items = [];
let descuento = 0;
// Funciones públicas (API del carrito)
return {
agregarItem(nombre, precio, cantidad = 1) {
items.push({ nombre, precio, cantidad });
console.log(`Agregado: ${nombre} x${cantidad}`);
},
quitarItem(nombre) {
items = items.filter(item => item.nombre !== nombre);
console.log(`Quitado: ${nombre}`);
},
aplicarDescuento(porcentaje) {
descuento = porcentaje / 100;
console.log(`Descuento del ${porcentaje}% aplicado`);
},
verResumen() {
const subtotal = items.reduce((sum, item) => sum + (item.precio * item.cantidad), 0);
const descuentoMonto = subtotal * descuento;
const total = subtotal - descuentoMonto;
console.log("\n=== RESUMEN DEL CARRITO ===");
console.table(items);
console.log(`Subtotal: $${subtotal.toFixed(2)}`);
if (descuento > 0) {
console.log(`Descuento: -$${descuentoMonto.toFixed(2)}`);
}
console.log(`Total: $${total.toFixed(2)}`);
return total;
},
get cantidadItems() {
return items.length;
}
};
}
const miCarrito = crearCarrito();
miCarrito.agregarItem("Laptop", 999.99);
miCarrito.agregarItem("Mouse", 29.99, 2);
miCarrito.agregarItem("Teclado", 79.99);
miCarrito.aplicarDescuento(10);
miCarrito.verResumen();
console.log(`Items en carrito: ${miCarrito.cantidadItems}`);
// items y descuento son inaccesibles desde fuera
// console.log(miCarrito.items); // undefined (¡privado!)
Memoización (cachear resultados)
function crearMemoizador(funcion) {
const cache = new Map();
return function(...args) {
const clave = JSON.stringify(args);
if (cache.has(clave)) {
console.log(`Cache hit para: ${clave}`);
return cache.get(clave);
}
console.log(`Calculando para: ${clave}`);
const resultado = funcion(...args);
cache.set(clave, resultado);
return resultado;
};
}
// Función costosa
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const fiboMemo = crearMemoizador(fibonacci);
console.time("Primera vez");
console.log(fiboMemo(35)); // Calcula
console.timeEnd("Primera vez");
console.time("Segunda vez");
console.log(fiboMemo(35)); // Cache!
console.timeEnd("Segunda vez");
IIFE (Immediately Invoked Function Expression)
Una función que se ejecuta inmediatamente después de crearse:
// Sintaxis básica
(function() {
const privado = "No contamino el scope global";
console.log(privado);
})();
// Con arrow function
(() => {
const config = { modo: "producción", version: "2.0" };
console.log("App inicializada:", config);
})();
// Útil para inicialización que no debe contaminar el scope global
const app = (() => {
const VERSION = "1.0.0";
const ENV = "production";
return {
getVersion: () => VERSION,
getEnv: () => ENV,
};
})();
console.log(app.getVersion()); // "1.0.0"
Errores comunes de principiantes
Crear closures dentro de loops con var: Las variables
varen loops comparten el mismo scope de función, causando bugs.No entender la TDZ: Acceder a
let/constantes de su declaración causa ReferenceError, no undefined.Abusar de variables globales: Cada variable global es una oportunidad para un bug. Encapsulá en funciones o módulos.
Confundir hoisting de var con let/const: Solo
vardaundefinedantes de declararse.let/constdan error.No aprovechar closures para encapsulación: Si necesitás datos privados, usá closures en vez de convenciones como
_variable.
Puntos clave de esta lección
- JavaScript tiene 3 niveles de scope: global, función y bloque.
- La cadena de scope permite acceder a variables de scopes superiores pero no inferiores.
- var se eleva con valor
undefined; let/const se elevan pero tienen TDZ (Temporal Dead Zone). - Las declaraciones de función se elevan completas; las expresiones y arrows no.
- Los closures permiten que funciones internas recuerden variables de funciones externas.
- El Module Pattern usa closures para crear variables y funciones privadas.
- IIFE ejecutan funciones inmediatamente y son útiles para evitar contaminar el scope global.
Quiz de autoevaluación
1. ¿Qué es la Temporal Dead Zone (TDZ)?
a) El tiempo que tarda en cargarse JavaScript
b) El espacio entre el inicio del bloque y la declaración de let/const
c) Un error de memoria
d) El scope global
2. ¿Qué valor tiene una variable var antes de su declaración?
a) null
b) Error
c) undefined
d) 0
3. ¿Qué es un closure?
a) Un bloque de código encerrado en {}
b) Una función que recuerda variables de su scope de creación
c) Un loop infinito
d) Una variable global
4. ¿Para qué sirve una IIFE?
a) Para crear loops
b) Para ejecutar una función inmediatamente sin contaminar el scope global
c) Para declarar variables
d) Para importar módulos
5. ¿Cuál es la cadena de scope correcta?
a) Bloque → Global → Función
b) Global → Función → Bloque
c) Bloque → Función → Global (búsqueda de adentro hacia afuera)
d) Función → Bloque → Global
Respuestas: 1-b, 2-c, 3-b, 4-b, 5-c
Revisemos los puntos más importantes de esta lección antes de continuar.
Ejercicio práctico
Misión: Crear un módulo de gestión de tareas
Usando el Module Pattern con closures, creá una función crearGestorTareas() que retorne un objeto con los siguientes métodos:
agregar(titulo, prioridad)— agrega una tarea (prioridad: "alta", "media", "baja")completar(id)— marca una tarea como completadaeliminar(id)— elimina una tarealistar(filtro)— lista tareas. Filtro puede ser "todas", "pendientes", "completadas"estadisticas()— retorna { total, completadas, pendientes, porcentajeCompletado }
Las tareas internas deben ser privadas (no accesibles desde fuera). Cada tarea debe tener un id autogenerado.
const tareas = crearGestorTareas();
tareas.agregar("Estudiar JavaScript", "alta");
tareas.agregar("Hacer ejercicio", "media");
tareas.agregar("Comprar víveres", "baja");
tareas.completar(1);
tareas.listar("todas");
console.log(tareas.estadisticas());
- Comprendo el concepto principal y puedo explicarlo con mis palabras
- Entiendo cómo aplicarlo en mi situación específica
- Necesito repasar algunas partes antes de continuar
- Quiero ver más ejemplos prácticos del tema