El scope en JavaScript determina desde dónde es accesible una variable. No hay un único scope: existen el scope global, el de función y el de bloque, y se organizan en una cadena jerárquica que el motor recorre para encontrar variables. Junto a esto, la Temporal Dead Zone y los closures completan el modelo de visibilidad del lenguaje.
Scope global, de función y de bloque
Las variables declaradas fuera de cualquier función o bloque pertenecen al scope global. Las declaradas con var dentro de una función tienen scope de función. Las declaradas con let o const tienen scope de bloque:
// Scope global
var globalVar = 'soy global (var)';
let globalLet = 'soy global (let)';
function ejemplo() {
// Scope de función
var funcionVar = 'scope de función';
let funcionLet = 'scope de función';
if (true) {
// Scope de bloque
var bloqueVar = 'var ignorar bloque'; // Sube al scope de función
let bloqueLet = 'let respeta bloque'; // Solo en el if
const bloqueConst = 'const respeta bloque';
console.log(bloqueVar); // OK
console.log(bloqueLet); // OK
console.log(bloqueConst); // OK
}
console.log(bloqueVar); // OK (var se eleva a la función)
console.log(bloqueLet); // ReferenceError (fuera del bloque)
}
ejemplo();
console.log(globalVar); // OK
console.log(globalLet); // OK
console.log(funcionVar); // ReferenceError (fuera de la función)
La scope chain: cómo el motor busca variables
Cuando el motor busca una variable, empieza en el scope actual y sube por la cadena hasta encontrarla o llegar al global. Esto es lo que hace posibles los closures:
const nivel1 = 'global';
function externa() {
const nivel2 = 'externa';
function media() {
const nivel3 = 'media';
function interna() {
const nivel4 = 'interna';
// Puede acceder a todos los niveles superiores (scope chain)
console.log(nivel4); // 'interna' scope propio
console.log(nivel3); // 'media' un nivel arriba
console.log(nivel2); // 'externa' dos niveles arriba
console.log(nivel1); // 'global' tres niveles arriba
}
interna();
}
media();
}
externa();
// La variable interna no es accesible desde fuera:
// console.log(nivel4); // ReferenceError
Temporal Dead Zone con let y const
Desde el inicio del bloque hasta la línea donde se declara la variable, let y const existen en la Temporal Dead Zone. Acceder a ellas en ese intervalo lanza ReferenceError:
{
// TDZ para 'x' empieza aquí
console.log(y); // undefined (var elevada, NO en TDZ)
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 5; // TDZ termina
var y = 10; // var se eleva al scope de función/global
console.log(x); // 5 ya accesible
}
// El bug clásico de TDZ con parámetros por defecto:
function ejemplo(a = b, b = 1) {
// a = b: b está en TDZ cuando se evalúa el parámetro a
// ReferenceError: Cannot access 'b' before initialization
}
function correcto(a = 1, b = a) { // b depende de a, no al revés
console.log(a, b);
}
Closures y scope
Un closure captura el scope donde se creó la función, no una copia de los valores. Si la variable capturada cambia, el closure ve el nuevo valor:
// El closure captura la referencia a la variable, no el valor
function crearContador(inicio = 0) {
let cuenta = inicio; // Variable capturada por el closure
return {
incrementar() { cuenta++; },
decrementar() { cuenta--; },
valor() { return cuenta; }
};
}
const c1 = crearContador(10);
const c2 = crearContador(0);
c1.incrementar();
c1.incrementar();
console.log(c1.valor()); // 12
console.log(c2.valor()); // 0 (scope independiente)
// El bug clásico: var en bucles
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 3, 3, 3
}
// Solución: let crea un binding por iteración
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 100); // 0, 1, 2
}
El patrón IIFE para crear scope aislado
Las Immediately Invoked Function Expressions crean un scope de función privado, útil en módulos legado o cuando necesitas evitar contaminar el scope global:
// IIFE: se define y se llama inmediatamente
const resultado = (function() {
const interno = 'no accesible desde fuera';
let contador = 0;
// Solo esto es accesible desde fuera:
return {
incrementar() { contador++; },
valor() { return contador; }
};
})();
resultado.incrementar();
console.log(resultado.valor()); // 1
console.log(resultado.interno); // undefined
// Con arrow function (más moderno):
const modulo = (() => {
const privado = 'secreto';
return { obtener: () => privado };
})();
El scope y los closures son conceptos entrelazados: entender uno ayuda a entender el otro. La regla fundamental: una función puede acceder a las variables del scope donde fue definida, no donde fue llamada. Este comportamiento léxico es lo que distingue a JavaScript de lenguajes con scope dinámico.
