Closures en JavaScript: cómo gestionar suscripciones y crear iteradores personalizados

Los closures son una de esas herramientas que, cuando se entienden bien, permiten escribir código más limpio y modular. En este artículo veremos dos casos de uso que aún no hemos cubierto: gestionar suscripciones (por ejemplo, a eventos o websockets) y crear iteradores personalizados sin necesidad de clases ni generadores.
1. Suscripciones con cancelación automática
Imagina que tienes un componente que se suscribe a un EventSource o a un WebSocket. Cuando el componente se desmonta, hay que cerrar la conexión para evitar fugas de memoria. Un closure nos permite encapsular tanto la lógica de suscripción como la función de cancelación.
function subscribeToChannel(channel, handler) {
const ws = new WebSocket(`wss://example.com/${channel}`);
ws.addEventListener('message', handler);
// El closure devuelve una función que cierra la conexión.
return function unsubscribe() {
ws.removeEventListener('message', handler);
ws.close();
console.log(`Desconectado de ${channel}`);
};
}
// Uso típico en un componente de React (pero sirve en cualquier contexto)
const stop = subscribeToChannel('news', e => console.log(e.data));
// …más tarde, cuando el componente se desmonta
stop();
La función unsubscribe mantiene una referencia al WebSocket y al handler gracias al closure, aunque esos valores ya no estén accesibles desde el ámbito exterior.
2. Iteradores personalizados sin generadores
Los iteradores son útiles cuando queremos recorrer una colección que no es un array, como una lista enlazada o un árbol. En vez de usar la sintaxis de generadores (function*), podemos crear un iterator con un closure que mantiene el estado interno.
function createRangeIterator(start, end, step = 1) {
let current = start;
return {
next() {
if (current > end) {
return { done: true };
}
const result = { value: current, done: false };
current += step;
return result;
}
};
}
const it = createRangeIterator(0, 5);
console.log(it.next()); // {value:0,done:false}
console.log(it.next()); // {value:1,done:false}
// …
El objeto devuelto contiene el método next, que accede a current mediante el closure. Cada llamada avanza el estado interno sin exponerlo al exterior.
3. Ventajas prácticas
- Encapsulación de recursos: la conexión o el índice del iterador quedan protegidos, evitando manipulaciones accidentales.
- API mínima: solo exponemos lo necesario (una función de cancelación o
next), lo que simplifica el contrato con quien consuma nuestro código. - Reutilización: el mismo patrón sirve para timers, suscripciones a Redux, o cualquier recurso que necesite limpieza.
4. Buenas prácticas al usar closures
- Documenta siempre qué recursos está cerrando la función devuelta.
- Evita crear closures dentro de bucles sin necesidad; eso genera cientos de funciones en memoria.
- Si el closure captura objetos grandes, considera usar
WeakMappara que el recolector de basura pueda liberar lo que ya no se necesita.
Con estos dos ejemplos tienes ahora dos herramientas más en tu caja: una forma segura de gestionar suscripciones y un patrón ligero para crear iteradores personalizados. Ambos aprovechan la capacidad de los closures para mantener estado privado y, al mismo tiempo, ofrecer una API simple y predecible. La próxima vez que necesites limpiar recursos o recorrer una estructura no convencional, prueba a encapsular la lógica en un closure; notarás la diferencia en legibilidad y mantenimiento.