Closures y Scope

Entiende el scope léxico de JavaScript, cómo las funciones recuerdan su entorno y los patrones prácticos con closures.

El scope determina qué variables son accesibles en cada punto del código. Los closures son el resultado natural del scope léxico: una función que recuerda las variables del entorno donde fue creada, aunque ese entorno ya no esté en la pila de llamadas.


Scope léxico

JavaScript usa scope léxico: el scope de una variable se determina en el momento de escribir el código, no de ejecutarlo. Una función puede acceder a las variables del bloque donde fue definida, no del bloque donde fue llamada:

const mensaje = 'global';

function externa() {
  const mensaje = 'externa';

  function interna() {
    // interna puede acceder a las variables de externa y del global
    console.log(mensaje);  // 'externa' — el scope léxico gana
  }

  interna();
}

externa();

La cadena de scope (scope chain)

Cuando JavaScript busca una variable, sube por la cadena de scopes hasta encontrarla o llegar al global:

const x = 1;  // global

function nivel1() {
  const y = 2;

  function nivel2() {
    const z = 3;

    // nivel2 ve: z (propio), y (nivel1), x (global)
    console.log(x, y, z);  // 1 2 3
  }

  // nivel1 ve: y (propio), x (global) — NO ve z
  nivel2();
}

Closures

Un closure ocurre cuando una función retorna otra función (o la guarda para usarla después). La función interna cierra sobre las variables de su entorno, manteniéndolas vivas:

function crearContador() {
  let cuenta = 0;  // privado — no accesible desde fuera

  return {
    incrementar() { cuenta++ },
    decrementar() { cuenta-- },
    valor() { return cuenta },
  };
}

const contador = crearContador();
contador.incrementar();
contador.incrementar();
contador.incrementar();
console.log(contador.valor());  // 3

// `cuenta` está viva porque las funciones retornadas la referencian
// pero no es accesible directamente — esto es encapsulación
console.log(contador.cuenta);  // undefined

Factory functions

Las closures permiten crear “fábricas” que generan funciones personalizadas:

function multiplicador(factor) {
  return (numero) => numero * factor;
}

const doble = multiplicador(2);
const triple = multiplicador(3);

console.log(doble(5));   // 10
console.log(triple(5));  // 15

// Cada función guarda su propia copia de `factor`
function saludar(saludo) {
  return (nombre) => `${saludo}, ${nombre}!`;
}

const saludarEnEspañol = saludar('Hola');
const saludarEnIngles = saludar('Hello');

console.log(saludarEnEspañol('Ana'));  // 'Hola, Ana!'
console.log(saludarEnIngles('Ana'));   // 'Hello, Ana!'

Memoización

Un closure puede cachear resultados de operaciones costosas:

function memoizar(fn) {
  const cache = new Map();  // el Map vive en el closure

  return function(...args) {
    const clave = JSON.stringify(args);

    if (cache.has(clave)) {
      console.log('cache hit');
      return cache.get(clave);
    }

    const resultado = fn(...args);
    cache.set(clave, resultado);
    return resultado;
  };
}

function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

const fibMemo = memoizar(fibonacci);
fibMemo(40);  // tarda
fibMemo(40);  // instantáneo — devuelve del cache

El problema de var en bucles

Un error clásico que ilustra perfectamente cómo funcionan los closures:

// ❌ Con var — un solo binding compartido
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Imprime: 3 3 3
// Por qué: var tiene scope de función, no de bloque
// Cuando los timers se ejecutan, el bucle ya terminó e i vale 3

// ✅ Con let — un binding por iteración
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Imprime: 0 1 2
// Por qué: let crea un nuevo scope por cada iteración del bucle

IIFE (Immediately Invoked Function Expression)

La forma antigua de crear scope privado antes de que existieran let/const y los módulos. Todavía aparece en código legado:

(function() {
  // Todo lo declarado aquí es privado
  const privado = 'no contamina el global';
  console.log(privado);
})();

// Con arrow function
(() => {
  const privado = 'también funciona';
})();

Hoy en día los módulos ES hacen lo mismo de forma más explícita.


Closures en el navegador: event listeners

function configurarBoton(elemento, mensaje) {
  // `mensaje` queda encerrado en el closure del listener
  elemento.addEventListener('click', () => {
    alert(mensaje);
  });
}

configurarBoton(document.querySelector('#btn1'), 'Clic en botón 1');
configurarBoton(document.querySelector('#btn2'), 'Clic en botón 2');
// Cada botón tiene su propio `mensaje` encerrado

Precaución: closures y memoria

Los closures mantienen vivas las variables referenciadas. Si una función guarda una referencia a un elemento DOM o un conjunto grande de datos, ese objeto no puede ser recolectado por el garbage collector:

// ❌ Puede causar memory leaks si se acumulan muchos listeners
function adjuntarListener(el) {
  const datosPesados = cargarMuchosKB();  // gran objeto

  el.addEventListener('click', () => {
    console.log(datosPesados);  // el closure mantiene datosPesados vivo
  });
}

// ✅ Extraer solo lo necesario
function adjuntarListener(el) {
  const id = cargarMuchosKB().id;  // solo guarda el id

  el.addEventListener('click', () => {
    console.log(id);  // closure pequeño
  });
}

En la última lección del curso aprendemos a manejar errores de forma robusta con try/catch, tipos de error y cómo crear errores personalizados.