JS 16: Closures (Clausuras)

El concepto más poderoso de JavaScript: funciones que recuerdan su entorno

1. ¿Qué es un Closure?

Un closure (clausura) es una función que tiene acceso a variables de su ámbito externo, incluso después de que la función externa haya terminado de ejecutarse.

Definición técnica: Un closure es la combinación de una función y el entorno léxico (lexical environment) en el que fue declarada. Este entorno consiste en todas las variables que estaban en el scope cuando se creó el closure.

¿Por qué son importantes?

Ejemplo Básico

function crearSaludo(saludo) {

  // Variable del ámbito externo

  

  return function(nombre) {

    // Esta función interna tiene acceso a 'saludo'

    console.log(saludo + ', ' + nombre);

  };

}



const saludarEnEspanol = crearSaludo('Hola');

const saludarEnIngles = crearSaludo('Hello');



saludarEnEspanol('Carlos'); // "Hola, Carlos"

saludarEnIngles('John');    // "Hello, John"



// ¡'saludo' sigue accesible desde las funciones internas!

Lo sorprendente: Aunque crearSaludo() ya terminó, la función retornada TODAVÍA recuerda el valor de saludo.

Visualización del Closure

function externa() {

  const mensaje = 'Hola desde externa';

  

  function interna() {

    console.log(mensaje); // Accede a 'mensaje'

  }

  

  return interna;

}



const miFuncion = externa(); // externa() termina

miFuncion(); // "Hola desde externa" - ¡Mensaje sigue vivo!

1.5. Visualización Paso a Paso

Veamos cómo se crea un closure paso a paso:

// PASO 1: Se declara la función externa
function crearMultiplicador(factor) {
  // PASO 2: Se crea una variable en el scope de crearMultiplicador
  // 'factor' vive aquí
  
  // PASO 3: Se declara y retorna una función interna
  return function(numero) {
    // PASO 4: Esta función "captura" el valor de 'factor'
    return numero * factor;
  };
  // PASO 5: crearMultiplicador() termina, pero 'factor' NO se destruye
}

// PASO 6: Llamamos a la función externa
const duplicar = crearMultiplicador(2);
// En este momento:
// - crearMultiplicador(2) se ejecutó y terminó
// - Pero la función retornada RECUERDA que factor = 2

// PASO 7: Usamos la función retornada
console.log(duplicar(5)); // 10
// ¡La función todavía tiene acceso a 'factor'!

// PASO 8: Cada llamada crea un closure independiente
const triplicar = crearMultiplicador(3);
console.log(triplicar(5)); // 15
console.log(duplicar(5));  // 10 (no afectado)

🔍 Diagrama Mental

// Cuando haces esto:
const duplicar = crearMultiplicador(2);

// JavaScript crea algo como:
duplicar = {
  funcion: function(numero) { return numero * factor; },
  mochila: { factor: 2 }  // ← El closure "lleva" esto
}

2. ¿Por Qué Funcionan los Closures?

En JavaScript, las funciones tienen acceso a:

  1. Variables locales (declaradas dentro de ella)
  2. Parámetros de la función
  3. Variables del ámbito externo (scope externo)
const global = 'Soy global';



function padre() {

  const variablePadre = 'Soy del padre';

  

  function hijo() {

    const variableHija = 'Soy del hijo';

    

    console.log(variableHija);  // ? Acceso directo

    console.log(variablePadre);  // ? Closure (ámbito externo)

    console.log(global);         // ? Variable global

  }

  

  return hijo;

}



const f = padre();

f();

// "Soy del hijo"

// "Soy del padre"

// "Soy global"

La "Mochila" de la Función

Cuando se crea un closure, la función interna lleva una "mochila" con todas las variables del entorno donde fue creada.

2.5. Scope Chain (Cadena de Ámbitos)

Los closures funcionan gracias a la cadena de ámbitos. Cuando una función busca una variable:

const global = 'nivel 0';

function nivel1() {
  const var1 = 'nivel 1';
  
  function nivel2() {
    const var2 = 'nivel 2';
    
    function nivel3() {
      const var3 = 'nivel 3';
      
      // Búsqueda de variables (scope chain):
      console.log(var3);   // 1. Busca en nivel3 → ✓ Encontrada
      console.log(var2);   // 2. No en nivel3, busca en nivel2 → ✓ Encontrada
      console.log(var1);   // 3. No en nivel3, no en nivel2, busca en nivel1 → ✓ Encontrada
      console.log(global); // 4. Sube hasta el scope global → ✓ Encontrada
      // console.log(noExiste); // 5. No existe en ningún nivel → Error
    }
    
    return nivel3;
  }
  
  return nivel2;
}

const fn2 = nivel1();
const fn3 = fn2();
fn3(); // Todas las variables son accesibles

📊 Visualización de la Cadena

// Cuando nivel3() busca una variable:

nivel3 scope: { var3: 'nivel 3' }
   ↓ (si no encuentra, sube)
nivel2 scope: { var2: 'nivel 2' }
   ↓ (si no encuentra, sube)
nivel1 scope: { var1: 'nivel 1' }
   ↓ (si no encuentra, sube)
global scope: { global: 'nivel 0' }
   ↓ (si no encuentra)
ReferenceError

3. Ejemplo Práctico: Contador Privado

Un closure permite crear variables "privadas" que solo son accesibles desde funciones específicas:

function crearContador() {

  let cuenta = 0; // Variable "privada"

  

  return {

    incrementar() {

      cuenta++;

      return cuenta;

    },

    decrementar() {

      cuenta--;

      return cuenta;

    },

    obtenerValor() {

      return cuenta;

    }

  };

}



const contador = crearContador();



console.log(contador.incrementar()); // 1

console.log(contador.incrementar()); // 2

console.log(contador.decrementar()); // 1

console.log(contador.obtenerValor()); // 1



// ? No puedes acceder directamente a 'cuenta'

console.log(contador.cuenta); // undefined



// La única forma de modificar 'cuenta' es usando los métodos

Múltiples Contadores Independientes

const contador1 = crearContador();

const contador2 = crearContador();



contador1.incrementar(); // 1

contador1.incrementar(); // 2



contador2.incrementar(); // 1



console.log(contador1.obtenerValor()); // 2

console.log(contador2.obtenerValor()); // 1



// Cada uno tiene su propio 'cuenta' privado!

4. Pattern: Module Pattern (Patrón Módulo)

Los closures permiten crear "módulos" con métodos públicos y privados:

const calculadora = (function() {

  // Variables y funciones privadas

  let resultado = 0;

  

  function validarNumero(num) {

    if (typeof num !== 'number') {

      throw new Error('Se esperaba un número');

    }

  }

  

  // API pública (lo que se retorna)

  return {

    sumar(num) {

      validarNumero(num);

      resultado += num;

      return this;

    },

    

    restar(num) {

      validarNumero(num);

      resultado -= num;

      return this;

    },

    

    multiplicar(num) {

      validarNumero(num);

      resultado *= num;

      return this;

    },

    

    obtenerResultado() {

      return resultado;

    },

    

    resetear() {

      resultado = 0;

      return this;

    }

  };

})(); // IIFE - se ejecuta inmediatamente



// Uso

calculadora

  .sumar(10)

  .multiplicar(5)

  .restar(20);

  

console.log(calculadora.obtenerResultado()); // 30



// ? No puedes acceder a las funciones privadas

// calculadora.validarNumero(5); // Error

5. Closures en Bucles (El Problema Clásico)

Un error común al usar closures con bucles:

? Problema

// Queremos crear 3 funciones que impriman 0, 1, 2

for (var i = 0; i < 3; i++) {

  setTimeout(function() {

    console.log(i); // ¿Qué imprimirá?

  }, 1000);

}



// Resultado: imprime "3" tres veces

// ¿Por qué? Todas las funciones comparten la MISMA 'i'

? Solución 1: let (Modern JavaScript)

for (let i = 0; i < 3; i++) {

  setTimeout(function() {

    console.log(i); // 0, 1, 2

  }, 1000);

}



// 'let' crea una nueva 'i' en cada iteración

? Solución 2: Closure con IIFE

for (var i = 0; i < 3; i++) {

  (function(j) {

    setTimeout(function() {

      console.log(j); // 0, 1, 2

    }, 1000);

  })(i); // Pasar 'i' como parámetro crea una copia

}

? Solución 3: Factory Function

function crearLogger(valor) {

  return function() {

    console.log(valor);

  };

}



for (var i = 0; i < 3; i++) {

  setTimeout(crearLogger(i), 1000);

}

5.5. Más Ejemplos de Closures en Bucles

Ejemplo Real: Crear Botones Dinámicamente

// ❌ PROBLEMA: Todos los botones mostrarán "Botón 5"
for (var i = 0; i < 5; i++) {
  const btn = document.createElement('button');
  btn.textContent = 'Botón ' + i;
  
  btn.addEventListener('click', function() {
    alert('Clickeaste el botón ' + i); // Siempre muestra 5
  });
  
  document.body.appendChild(btn);
}

// ✅ SOLUCIÓN 1: Usar let
for (let i = 0; i < 5; i++) {
  const btn = document.createElement('button');
  btn.textContent = 'Botón ' + i;
  
  btn.addEventListener('click', function() {
    alert('Clickeaste el botón ' + i); // Funciona correctamente
  });
  
  document.body.appendChild(btn);
}

// ✅ SOLUCIÓN 2: Closure con función auxiliar
function crearBotonHandler(indice) {
  return function() {
    alert('Clickeaste el botón ' + indice);
  };
}

for (var i = 0; i < 5; i++) {
  const btn = document.createElement('button');
  btn.textContent = 'Botón ' + i;
  btn.addEventListener('click', crearBotonHandler(i));
  document.body.appendChild(btn);
}

// ✅ SOLUCIÓN 3: Dataset (alternativa moderna)
for (var i = 0; i < 5; i++) {
  const btn = document.createElement('button');
  btn.textContent = 'Botón ' + i;
  btn.dataset.indice = i; // Guardar en el elemento
  
  btn.addEventListener('click', function() {
    alert('Clickeaste el botón ' + this.dataset.indice);
  });
  
  document.body.appendChild(btn);
}

6. Use Cases Reales de Closures

1. Factory Functions (Generadores)

function crearMultiplicador(factor) {

  return function(numero) {

    return numero * factor;

  };

}



const duplicar = crearMultiplicador(2);

const triplicar = crearMultiplicador(3);



console.log(duplicar(5));  // 10

console.log(triplicar(5)); // 15

2. Configuración Inicial

function configurarAPI(apiKey, baseURL) {

  return {

    get(endpoint) {

      return fetch(`${baseURL}${endpoint}`, {

        headers: { 'Authorization': `Bearer ${apiKey}` }

      });

    },

    

    post(endpoint, data) {

      return fetch(`${baseURL}${endpoint}`, {

        method: 'POST',

        headers: {

          'Authorization': `Bearer ${apiKey}`,

          'Content-Type': 'application/json'

        },

        body: JSON.stringify(data)

      });

    }

  };

}



const api = configurarAPI('abc123', 'https://api.example.com');



// apiKey y baseURL están "recordados" en el closure

api.get('/users');

api.post('/users', { name: 'Juan' });

3. Event Handlers

function crearBoton(etiqueta, callback) {

  const boton = document.createElement('button');

  boton.textContent = etiqueta;

  

  let clics = 0; // Variable privada

  

  boton.addEventListener('click', function() {

    clics++;

    console.log(`${etiqueta} clickeado ${clics} veces`);

    callback(clics);

  });

  

  return boton;

}



const btn = crearBoton('Mi Botón', (total) => {

  console.log('Total de clics:', total);

});



document.body.appendChild(btn);

4. Debouncing (Retrasar Ejecución)

function debounce(func, delay) {

  let timeoutId; // Se mantiene entre llamadas

  

  return function(...args) {

    clearTimeout(timeoutId);

    

    timeoutId = setTimeout(() => {

      func.apply(this, args);

    }, delay);

  };

}



// Uso: buscar solo después de que el usuario deje de escribir

const buscar = debounce((termino) => {

  console.log('Buscando:', termino);

  // Llamada a API

}, 500);



// Input de búsqueda

document.getElementById('busqueda')?.addEventListener('input', function(e) {

  buscar(e.target.value);

});

5. Memoization (Cachear Resultados)

// Memoization: Guardar resultados de funciones costosas
function memoize(fn) {

  const cache = {}; // Closure mantiene el cache

  

  return function(...args) {

    const key = JSON.stringify(args);

    

    if (key in cache) {

      console.log('Desde cache');

      return cache[key];

    }

    

    console.log('Calculando...');

    const resultado = fn.apply(this, args);

    cache[key] = 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(40)); // Calculando... (tarda)

console.log(fibMemo(40)); // Desde cache (instantáneo)

6. Throttling (Limitar Frecuencia de Ejecución)

function throttle(func, delay) {
  let ultimaEjecucion = 0;
  
  return function(...args) {
    const ahora = Date.now();
    
    if (ahora - ultimaEjecucion >= delay) {
      func.apply(this, args);
      ultimaEjecucion = ahora;
    }
  };
}

// Uso: Limitar scroll events
const handleScroll = throttle(() => {
  console.log('Scroll detectado');
}, 1000); // Máximo 1 vez por segundo

window.addEventListener('scroll', handleScroll);

7. Partial Application (Aplicación Parcial)

// Crear funciones especializadas a partir de funciones genéricas
function aplicacionParcial(fn, ...argsIniciales) {
  return function(...argsFinales) {
    return fn(...argsIniciales, ...argsFinales);
  };
}

// Función genérica
function saludar(saludo, nombre, puntuacion) {
  return `${saludo}, ${nombre}${puntuacion}`;
}

// Crear versiones especializadas
const saludoFormal = aplicacionParcial(saludar, 'Buenos días');
const saludoCasual = aplicacionParcial(saludar, 'Hola');

console.log(saludoFormal('Sr. García', '.')); // "Buenos días, Sr. García."
console.log(saludoCasual('Juan', '!')); // "Hola, Juan!"

8. Singleton Pattern (Instancia Única)

const Singleton = (function() {
  let instancia; // Variable privada
  
  function crearInstancia() {
    return {
      nombre: 'Mi Singleton',
      metodo() {
        console.log('Método del singleton');
      }
    };
  }
  
  return {
    obtenerInstancia() {
      if (!instancia) {
        instancia = crearInstancia();
      }
      return instancia;
    }
  };
})();

const obj1 = Singleton.obtenerInstancia();
const obj2 = Singleton.obtenerInstancia();

console.log(obj1 === obj2); // true - misma instancia

9. Iteradores Personalizados

function crearIterador(array) {
  let indice = 0;
  
  return {
    siguiente() {
      if (indice < array.length) {
        return { valor: array[indice++], terminado: false };
      }
      return { valor: undefined, terminado: true };
    },
    
    reiniciar() {
      indice = 0;
    },
    
    tieneElementos() {
      return indice < array.length;
    }
  };
}

const numeros = crearIterador([1, 2, 3, 4, 5]);

console.log(numeros.siguiente()); // { valor: 1, terminado: false }
console.log(numeros.siguiente()); // { valor: 2, terminado: false }
numeros.reiniciar();
console.log(numeros.siguiente()); // { valor: 1, terminado: false }

10. Gestión de Estado en Componentes

function crearComponente(nombreInicial) {
  // Estado privado
  let estado = {
    nombre: nombreInicial,
    contador: 0,
    activo: true
  };
  
  // Listeners privados
  const listeners = [];
  
  function notificarCambios() {
    listeners.forEach(listener => listener(estado));
  }
  
  // API pública
  return {
    obtenerEstado() {
      return { ...estado }; // Copia para evitar mutaciones
    },
    
    actualizarNombre(nuevoNombre) {
      estado.nombre = nuevoNombre;
      notificarCambios();
    },
    
    incrementar() {
      estado.contador++;
      notificarCambios();
    },
    
    alternar() {
      estado.activo = !estado.activo;
      notificarCambios();
    },
    
    suscribirse(listener) {
      listeners.push(listener);
      // Retornar función para desuscribirse
      return () => {
        const indice = listeners.indexOf(listener);
        if (indice > -1) listeners.splice(indice, 1);
      };
    }
  };
}

// Uso
const componente = crearComponente('MiComponente');

const desuscribir = componente.suscribirse((estado) => {
  console.log('Estado actualizado:', estado);
});

componente.incrementar(); // Log: Estado actualizado: { nombre: 'MiComponente', contador: 1, activo: true }
componente.actualizarNombre('Nuevo'); // Log: Estado actualizado: { nombre: 'Nuevo', contador: 1, activo: true }

desuscribir(); // Ya no recibirá notificaciones

7. Closures con Arrow Functions

Las arrow functions también crean closures:

const crearSumador = (base) => (numero) => base + numero;



const sumar5 = crearSumador(5);

const sumar10 = crearSumador(10);



console.log(sumar5(3));  // 8

console.log(sumar10(3)); // 13

Currying con Closures

// Transformar función de múltiples parámetros en funciones de un parámetro

const curry3 = (fn) => (a) => (b) => (c) => fn(a, b, c);



function sumar3Numeros(a, b, c) {

  return a + b + c;

}



const sumarCurried = curry3(sumar3Numeros);



// Uso

console.log(sumarCurried(1)(2)(3)); // 6



// O aplicación parcial

const suma1 = sumarCurried(1);

const suma1y2 = suma1(2);

console.log(suma1y2(3)); // 6

8. Consideraciones de Memoria

Los closures mantienen referencias a variables, lo que puede causar memory leaks si no se tiene cuidado:

?? Posible Memory Leak

function crearElementoGrande() {

  const datosGrandes = new Array(1000000).fill('X'); // Mucho memoria

  

  return function() {

    // Esta función mantiene 'datosGrandes' en memoria

    console.log(datosGrandes.length);

  };

}



const funciones = [];

for (let i = 0; i < 100; i++) {

  funciones.push(crearElementoGrande()); // ¡100 arrays grandes en memoria!

}

? Mejor: Liberar Referencias

function crearElementoOptimizado() {

  const datosGrandes = new Array(1000000).fill('X');

  const longitud = datosGrandes.length; // Copiar solo lo necesario

  

  return function() {

    console.log(longitud); // Solo guarda el número, no el array

  };

}

Limpiar Event Listeners

function setupListener() {

  const datosGrandes = new Array(1000000);

  

  const handler = () => console.log(datosGrandes.length);

  

  document.addEventListener('click', handler);

  

  // Retornar función para limpiar

  return () => document.removeEventListener('click', handler);

}



const cleanup = setupListener();



// Más tarde, liberar memoria

cleanup();

9. Ejercicios Prácticos

Ejercicio 1: Generador de IDs

Crea una función que genere IDs únicos consecutivos:

function crearGeneradorID() {

  // Tu código aquí

  // Debe mantener un contador privado

}



const generarID = crearGeneradorID();

console.log(generarID()); // 1

console.log(generarID()); // 2

console.log(generarID()); // 3

Ejercicio 2: Constructor de Saludo

Crea una función que construya saludos personalizados:

function crearSaludador(prefijo) {

  // Retorna una función que salude con el prefijo

}



const saludoFormal = crearSaludador('Estimado');

const saludoCasual = crearSaludador('Hola');



console.log(saludoFormal('Sr. García')); // "Estimado Sr. García"

console.log(saludoCasual('Juan'));       // "Hola Juan"

Ejercicio 3: Cache Simple

Implementa una función que cachee resultados:

function crearCache() {

  // Debe poder: set(key, value), get(key), has(key)

  // Usa un closure para mantener los datos privados

}



const cache = crearCache();

cache.set('usuario', { nombre: 'Ana' });

console.log(cache.get('usuario')); // { nombre: 'Ana' }

console.log(cache.has('usuario')); // true

Resumen

Los closures son uno de los conceptos MÁS IMPORTANTES de JavaScript. Una vez que los domines, entenderás mucho mejor cómo funciona el lenguaje.

Volver a JS