Articulo publicado el
Domina los cierres y escribe código más limpio y poderoso
Los closures son uno de los conceptos más poderosos y, al mismo tiempo, más confusos de JavaScript. Si has trabajado con JavaScript por un tiempo, probablemente ya los has usado sin siquiera darte cuenta. En esta guía, desglosaré qué son, cómo funcionan y, lo más importante, cuándo y cómo usarlos en tus proyectos.
Un closure es una función que tiene acceso a variables de su función padre, incluso después de que esa función ha terminado de ejecutarse. Esto ocurre porque en JavaScript, las funciones crean su propio scope, y las variables de ese scope persisten mientras exista una referencia a la función interna.
Veamos un ejemplo simple:
function saludar(nombre) {
const mensaje = `Hola, ${nombre}`;
return function () {
console.log(mensaje);
};
}
const saludo = saludar("Ana");
saludo(); // Output: "Hola, Ana"
En este ejemplo, la función interna tiene acceso a nombre y mensaje, que son variables del scope de la función externa. Aunque saludar() ya ha terminado de ejecutarse, la función interna aún puede acceder a esas variables. Eso es un closure.
Para entender realmente cómo funcionan los closures, es importante comprender qué sucede con el scope en JavaScript:
Cada función en JavaScript crea su propio scope. Cuando buscas una variable, JavaScript la busca en:
Esta cadena de búsqueda se llama el scope chain. Los closures aprovechan esto manteniendo viva la referencia al scope externo.
const global = "global";
function externa() {
const externa_var = "externa";
function interna() {
const interna_var = "interna";
console.log(interna_var); // "interna"
console.log(externa_var); // "externa"
console.log(global); // "global"
}
return interna;
}
const closure = externa();
closure(); // Todos los console.log funcionan correctamente
Lo importante es que las variables en el scope de la función padre no se descartan cuando la función termina. Mientras exista una referencia a la función interna, esas variables permanecerán en memoria.
function crearContador() {
let contador = 0; // Esta variable persiste
return function () {
contador++;
return contador;
};
}
const miContador = crearContador();
console.log(miContador()); // 1
console.log(miContador()); // 2
console.log(miContador()); // 3
Los closures son perfectos para mantener datos privados que no pueden ser accedidos directamente desde el exterior:
function crearCuenta(saldoInicial) {
let saldo = saldoInicial; // Variable privada
return {
depositar(cantidad) {
saldo += cantidad;
return saldo;
},
retirar(cantidad) {
if (cantidad <= saldo) {
saldo -= cantidad;
return saldo;
}
return "Saldo insuficiente";
},
obtenerSaldo() {
return saldo;
}
};
}
const miCuenta = crearCuenta(1000);
console.log(miCuenta.depositar(500)); // 1500
console.log(miCuenta.obtenerSaldo()); // 1500
console.log(miCuenta.saldo); // undefined (privada)
function crearOperacion(operador) {
return function (a, b) {
switch (operador) {
case "+":
return a + b;
case "-":
return a - b;
case "*":
return a * b;
default:
return null;
}
};
}
const sumar = crearOperacion("+");
const restar = crearOperacion("-");
console.log(sumar(10, 5)); // 15
console.log(restar(10, 5)); // 5
Los closures son esenciales para mantener contexto en callbacks:
function crearBotonMultiple(id, titulo) {
const boton = document.getElementById(id);
let clickCount = 0;
boton.addEventListener("click", function () {
clickCount++;
console.log(`${titulo} ha sido clickeado ${clickCount} veces`);
});
}
crearBotonMultiple("btn1", "Botón Principal");
crearBotonMultiple("btn2", "Botón Secundario");
Cada listener mantiene su propio clickCount gracias a los closures.
function debounce(fn, delay) {
let timeoutId; // Variable privada que persiste
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}
const buscar = debounce(function (query) {
console.log("Buscando:", query);
}, 500);
// Cada vez que se escribe, se cancela el timeout anterior
const miModulo = (function () {
const apiKey = "clave-secreta"; // Privada
let cachedData = null; // Privada
return {
obtenerDatos(url) {
if (cachedData) return cachedData;
// Usar apiKey internamente
cachedData = fetch(`${url}?key=${apiKey}`);
return cachedData;
},
limpiarCache() {
cachedData = null;
}
};
})();
// Evitar exceso de closures anidados
function nivel1() {
return function nivel2() {
return function nivel3() {
return function nivel4() {
// Demasiado complejo
};
};
};
}
Los closures pueden causar memory leaks si no tienes cuidado:
// ❌ Problema: mantiene referencia a todo el DOM
function crearLlamador(elemento) {
return function () {
console.log(elemento.innerText); // elemento nunca se libera
};
}
// ✅ Solución: extrae solo lo necesario
function crearLlamador(elemento) {
const texto = elemento.innerText;
return function () {
console.log(texto);
};
}
En JavaScript moderno, a menudo tienes opciones:
// Con Closure
function crearPersona(nombre) {
return {
obtenerNombre() {
return nombre;
}
};
}
// Con Clase
class Persona {
constructor(nombre) {
this.nombre = nombre;
}
obtenerNombre() {
return this.nombre;
}
}
Ambas funcionan, pero las clases suelen ser más legibles y debuggeables.
Los closures son una característica fundamental de JavaScript que te permite escribir código más modular, seguro y elegante. No son complicados una vez que entiendes la idea central: las funciones internas tienen acceso a las variables de sus funciones padre, y esas variables persisten mientras exista una referencia a la función interna.
Usa closures para encapsular datos, mantener estado privado y crear abstracciones poderosas. Pero siempre ten en mente la legibilidad de tu código y las alternativas modernas como las clases y los módulos ES6.
Estos articulos pueden interesarte si te gusto este articulo.
Optimiza el rendimiento de tu aplicación limitando cuántas veces se ejecutan listeners de eventos y funciones costosas
Cómo reducir listeners y mejorar el rendimiento usando bubbling de eventos
Domina la programación asíncrona desde cero