Scope en JavaScript: global, función, bloque y la temporal dead zone

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.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP