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?
- ✅ Permiten crear variables privadas (encapsulación)
- ✅ Mantienen estado entre llamadas a funciones
- ✅ Son la base de patrones como factory functions y module pattern
- ✅ Fundamentales para programación funcional en JavaScript
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:
- Variables locales (declaradas dentro de ella)
- Parámetros de la función
- 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
- ? Un closure es una función que recuerda su entorno léxico
- ? Permite crear variables "privadas"
- ? Fundamental para: factory functions, module pattern, event handlers
- ? Útil para: debouncing, memoization, configuraciones
- ? Arrow functions también crean closures
- ?? Cuidado con memory leaks si mantienes referencias innecesarias
- ? Con
leten bucles evitas problemas de closures
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.