TechLead

Closures y Ámbito Léxico

Comprender cómo los closures capturan y preservan el ámbito, su funcionamiento interno y aplicaciones prácticas

¿Qué son los Closures?

Un closure es una función que tiene acceso a variables en su ámbito externo, incluso después de que la función externa haya terminado de ejecutarse. Los closures son fundamentales para JavaScript y permiten patrones poderosos como privacidad de datos, fábricas de funciones y currying.

Conceptos Clave

  • Ámbito Léxico — Las funciones tienen acceso a variables definidas en su ámbito externo
  • Preservación del Estado — Los closures "recuerdan" su entorno de creación
  • Privacidad de Datos — Encapsula variables que no son accesibles desde el exterior
  • Fábricas de Funciones — Crea funciones con comportamiento preconfigurado

Ámbito Léxico Básico

function exterior() {
  const mensaje = "Hola desde exterior";
  
  function interior() {
    // Tiene acceso a 'mensaje' desde el ámbito exterior
    console.log(mensaje);
  }
  
  interior(); // "Hola desde exterior"
}

exterior();

// El closure en acción
function hacerSaludo(nombre) {
  const saludo = "Hola";
  
  return function() {
    // Esta función es un closure
    // "Recuerda" 'saludo' y 'nombre' incluso después de que hacerSaludo() termine
    console.log(`${saludo}, ${nombre}!`);
  };
}

const saludarAlice = hacerSaludo("Alice");
const saludarBob = hacerSaludo("Bob");

saludarAlice(); // "Hola, Alice!"
saludarBob();   // "Hola, Bob!"

Ejemplo del Mundo Real: Privacidad de Datos

function crearCuentaBancaria(saldoInicial) {
  // Variable privada - no accesible desde el exterior
  let saldo = saldoInicial;
  
  // API pública a través de closures
  return {
    depositar(monto) {
      if (monto > 0) {
        saldo += monto;
        console.log(`Depositado: $${monto}. Nuevo saldo: $${saldo}`);
      }
    },
    
    retirar(monto) {
      if (monto > 0 && monto <= saldo) {
        saldo -= monto;
        console.log(`Retirado: $${monto}. Saldo restante: $${saldo}`);
      } else {
        console.log("Fondos insuficientes");
      }
    },
    
    obtenerSaldo() {
      return saldo;
    }
  };
}

const miCuenta = crearCuentaBancaria(100);

miCuenta.depositar(50);        // Depositado: $50. Nuevo saldo: $150
miCuenta.retirar(30);          // Retirado: $30. Saldo restante: $120
console.log(miCuenta.obtenerSaldo()); // 120

// No se puede acceder a 'saldo' directamente
console.log(miCuenta.saldo);   // undefined

Fábrica de Funciones

// Crear funciones especializadas usando closures
function crearMultiplicador(factor) {
  return function(numero) {
    return numero * factor;
  };
}

const duplicar = crearMultiplicador(2);
const triplicar = crearMultiplicador(3);
const cuadruplicar = crearMultiplicador(4);

console.log(duplicar(5));     // 10
console.log(triplicar(5));    // 15
console.log(cuadruplicar(5)); // 20

// Ejemplo práctico: formateo con closure
function crearFormateadorMoneda(simbolo, decimales = 2) {
  return function(monto) {
    return `${simbolo}${monto.toFixed(decimales)}`;
  };
}

const formatoUSD = crearFormateadorMoneda("$");
const formatoEUR = crearFormateadorMoneda("€");
const formatoYEN = crearFormateadorMoneda("¥", 0);

console.log(formatoUSD(42.5));  // "$42.50"
console.log(formatoEUR(42.5));  // "€42.50"
console.log(formatoYEN(42.5));  // "¥43"

Memoización con Closures

// Cachear resultados de funciones costosas
function memoize(fn) {
  const cache = {}; // Variable privada en el closure
  
  return function(...args) {
    const clave = JSON.stringify(args);
    
    if (clave in cache) {
      console.log("Desde caché:", clave);
      return cache[clave];
    }
    
    const resultado = fn(...args);
    cache[clave] = resultado;
    return resultado;
  };
}

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

const fibMemo = memoize(fibonacci);

console.log(fibMemo(10));  // Calcula
console.log(fibMemo(10));  // Desde caché
console.log(fibMemo(11));  // Calcula solo nuevos valores

Errores Comunes: Closures en Bucles

// ❌ PROBLEMA: var y closures en bucles
for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // Imprime: 3, 3, 3 (no 0, 1, 2)
  }, 100);
}
// Problema: todos los closures comparten la misma variable 'i'

// ✅ SOLUCIÓN 1: Usar 'let' (ámbito de bloque)
for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // Imprime: 0, 1, 2 ✓
  }, 100);
}

// ✅ SOLUCIÓN 2: IIFE (Expresión de Función Inmediatamente Invocada)
for (var i = 0; i < 3; i++) {
  (function(index) {
    setTimeout(function() {
      console.log(index); // Imprime: 0, 1, 2 ✓
    }, 100);
  })(i);
}

// ✅ SOLUCIÓN 3: Parámetro de función
for (var i = 0; i < 3; i++) {
  setTimeout(function(index) {
    console.log(index); // Imprime: 0, 1, 2 ✓
  }, 100, i);
}

Closures en Manejadores de Eventos

// Problema: perder contexto en callbacks
function configurarBotones() {
  for (var i = 1; i <= 3; i++) {
    const boton = document.getElementById(`btn${i}`);
    
    // ❌ MALO: closure sobre 'i' mutable
    boton.addEventListener('click', function() {
      console.log(`Botón ${i} clicado`); // i será 4 para todos
    });
  }
}

// ✅ BUENO: usar let o crear un closure apropiado
function configurarBotones() {
  for (let i = 1; i <= 3; i++) {
    const boton = document.getElementById(`btn${i}`);
    
    boton.addEventListener('click', function() {
      console.log(`Botón ${i} clicado`); // Funciona correctamente
    });
  }
}

// O con fábrica de funciones
function crearManejadorClick(indice) {
  return function() {
    console.log(`Botón ${indice} clicado`);
  };
}

for (var i = 1; i <= 3; i++) {
  const boton = document.getElementById(`btn${i}`);
  boton.addEventListener('click', crearManejadorClick(i));
}

💡 Puntos Clave

  • • Los closures permiten que las funciones accedan a variables de ámbitos externos
  • • Son perfectos para privacidad de datos y encapsulación
  • • Las fábricas de funciones usan closures para crear funciones especializadas
  • • Cuidado con var en bucles - usa let o IIFEs
  • • Los closures se usan ampliamente en callbacks, manejadores de eventos y programación funcional
  • • Cada función forma un closure sobre su ámbito léxico

Ejercicios rápidos de práctica

  • • Crea un contador privado con métodos incrementar() y obtenerValor() usando closures.
  • • Escribe una función memoize que cachee resultados de funciones costosas.
  • • Construye una fábrica de funciones que cree temporizadores con diferentes intervalos.
  • • Soluciona el problema clásico del bucle adjuntando event listeners únicos a múltiples elementos.