Aprende a dominar los closures en JavaScript con un ejemplo claro y práctico: una cuenta bancaria con estado privado. Aquí verás cómo una función recuerda su scope incluso después de ejecutarse, permitiendo encapsular datos, exponer métodos seguros y mantener consistencia del estado.
¿Qué es un closure y por qué importa en JavaScript?
Un closure es “una función que recuerda el scope en el cual fue creada, incluso después de que esta ha sido ejecutada”. En este caso, la variable saldo se define como privada y solo es accesible por los métodos retornados. Así se evita exponer directamente el estado y se controlan las operaciones disponibles.
¿Cómo recuerda el scope y mantiene estado privado?
Se define let saldo = saldoInicial dentro de la función creadora.
Los métodos retornados acceden a saldo gracias al closure.
El estado persiste entre invocaciones: cada depósito o retiro actualiza saldo sin exponerlo públicamente.
¿Qué operaciones se habilitan con estado privado?
depositar(cantidad): suma al saldo y devuelve un mensaje con el nuevo total.
retirar(cantidad): valida fondos, evita “dinero infinito” y actualiza el saldo.
consultarSaldo(): retorna el saldo actual en formato legible.
¿Cómo crear una cuenta bancaria con closure en JavaScript?
A continuación, la función que crea la cuenta, encapsula saldo y retorna métodos para operar con la cuenta. Observa cómo cada método accede a saldo gracias al closure.
El profesor publicó las soluciones pero no incluyó el archivo del reto.
Si quieren intentar el reto de closures sin ver las soluciones, hice el archivo del challenge con instrucciones para que puedan resolverlos.
Código aquí:
NOTA: Copia todo el contenido y reemplázalo en el archivo principal.
Aunque el ejercicio no estaba, me sirvió analizar los resultados e intentar replicar la lógica, utilizando el archivo de Marco Antonio.
Añadí comentarios e intenté cambiar la forma de demostrar los resultados
// ============================================// Reto 15 — Closures en JavaScript (Estado Privado)// ============================================// En estos ejercicios practicarás closures en JavaScript.// Un closure permite que una función recuerde variables// definidas dentro de ella incluso después de ejecutarse.// La idea es crear variables privadas que solo puedan// modificarse mediante métodos.// Ejecuta los tests con:// npx vitest src/15-closures-javascript// ============================================// ============================================// Nivel 1 — Básico// ============================================// --- Reto 1: Contador Privado ---// Dentro de la función declara una variable privada:// let contador = 0;// La función debe retornar un objeto con los métodos:// incrementar()// - aumenta el contador en 1// - retorna el nuevo valor// decrementar()// - resta 1 al contador// - retorna el nuevo valor// obtenerValor()// - retorna el valor actual del contador// Pista:// Las funciones deben poder acceder a la variable contador// gracias al closure.functioncrearContador(){// tu codigo aquílet contador=0;return{incrementar(){++contador;return contador;},decrementar(){--contador;return contador;},obtenerValor(){return contador;}}}const miContador=crearContador();console.log(miContador.incrementar());console.log(miContador.obtenerValor());console.log(miContador.decrementar());console.log(miContador.obtenerValor());// --- Reto 2: Acumulador ---// Declara una variable privada:// let total = 0;// Retorna un objeto con:// sumar(valor)// - suma valor al total// - retorna el nuevo total// total()// - retorna el valor acumulado// Ejemplo:// const acumulador = crearAcumulador()// acumulador.total() // 0// acumulador.sumar(10) // 10// acumulador.sumar(5) // 15functioncrearAcumulador(){// tu codigo aquílet total=0;return{sumar(valor){ total+=valor;return total;},total(){return total;}}}const miAcumulador=crearAcumulador();console.log(miAcumulador.sumar(100));console.log(miAcumulador.sumar(200));console.log(miAcumulador.total());// ============================================// Nivel 2 — Intermedio// ============================================// --- Reto 3: Cuenta Bancaria ---// Declara una variable privada:// let saldo = saldoInicial;// Retorna un objeto con los métodos:// depositar(cantidad)// - suma la cantidad al saldo// - retorna:// "Depositado $cantidad. Saldo actual: $saldo."// retirar(cantidad)// - si la cantidad es mayor al saldo retorna:// "Fondos insuficientes."// - si hay fondos suficientes:// resta la cantidad y retorna:// "Retirado $cantidad. Saldo actual: $saldo."// consultarSaldo()// - retorna:// "Saldo: $saldo."functioncrearCuentaBancaria(saldoInicial){// tu codigo aquílet saldo=saldoInicial;return{depositar(cantidad){ saldo+=cantidad;return`Se depositó $${cantidad}. Saldo actual $${saldo}`;},retirar(cantidad){if(cantidad<saldo){ saldo-=cantidad;return`Se retiró $${cantidad}. Saldo: $${saldo}`;}else{return`Fondos insuficiente`;}},consultarSaldo(){return`El saldo es $${saldo}`;}}}const miCuenta=crearCuentaBancaria(1000);console.log(miCuenta.depositar(2000));console.log(miCuenta.retirar(400));console.log(miCuenta.consultarSaldo());//crear carrito de comprasfunctioncrearCarrito(){let productos=[];return{agregar({producto,precio}){ productos.push({nombre:producto,precio:precio});return`${producto} agregado al carrito`;},remover(nombre){ productos=productos.filter((p)=> p.nombre!==nombre);return`${nombre} removido del carrito`;},total(){return`el total del carrito es ${productos.reduce((sum,p)=> sum + p.precio,0)}`;/*
Aquí está la estructura de reduce: productos.reduce((sum, p) => sum + p.precio, 0);
sum (El acumulador): Es tu "caja". Empieza valiendo 0 (ese es el número después de la coma).
p (El elemento actual): Es el producto que el cajero está revisando en ese momento.
=> sum + p.precio: Es la instrucción. Por cada producto, toma lo que ya hay en la caja (sum) y súmale el precio del producto actual (p.precio).
0: Es el valor inicial de la caja. Si no lo pones, reduce tomaría el primer producto como inicio, lo cual suele causar errores.
*/},vaciar(){ productos =[];return`el carrito está vacío`}};}const miCarrito=crearCarrito();console.log(miCarrito.agregar({producto:"galletas",precio:100}));console.log(miCarrito.agregar({producto:"dulces",precio:50}));console.log(miCarrito.remover("dulces"));console.log(miCarrito.total());console.log(miCarrito.vaciar());// --- Reto 5: Cache Simple ---// Declara un objeto privado:// let datos = {};// Retorna un objeto con:// guardar(clave, valor)// - guarda el valor usando:// datos[clave] = valor// - retorna:// "Valor guardado en clave: clave"// obtener(clave)// - retorna el valor asociado a la clave// existe(clave)// - retorna true o false dependiendo si la clave existe// - Pista: usa - in - // limpiar()// - elimina todos los datos del cachefunctioncrearCache(){// tu codigo aquílet datos={};return{guardar(clave,valor){ datos[clave]=valor;//Asigna un valor a una clave dentro de datos. Es como decir: "guarda este dato bajo este nombre".return`Valor guardado en clave: ${clave}`;},obtener(clave){return datos[clave];//Busca en datos lo que guardaste bajo esa clave.},existe(clave){return clave in datos;//Usa el operador in para verificar si la clave ya vive dentro de datos (devuelve true o false).},limpiar(){ datos={};return'Cache limpiado.';}};}const miCache=crearCache();console.log(miCache.guardar('A','1'));console.log(miCache.obtener('A'));console.log(miCache.existe('A'));console.log(miCache.limpiar());// --- Reto 6: Temporizador con Estado ---// Declara variables privadas:// let segundos = 0// let intervalo = null// let corriendo = false// Retorna un objeto con:// iniciar()// si el temporizador no está corriendo:// - usa setInterval para incrementar segundos// detener()// si el temporizador está corriendo:// - detiene el temporizador usando clearInterval()// reiniciar()// - reinicia segundos a 0// - retorna: 'Temporizador reiniciado.';// obtenerTiempo()// - retorna los segundos actualesfunctioncrearTemporizador(){let segundos=0;let intervalo =null;let corriendo =false;return{iniciar(){if(!corriendo){ corriendo=true; intervalo =setInterval(()=>{segundos++;},1000);//La función () => { segundos++; }: Es la "tarea". Cada vez que suena la alarma, JavaScript ejecuta este bloque de código. En este caso, simplemente suma 1 a tu contador.//El número 1000: Es el tiempo en milisegundos. Como 1000 milisegundos son 1 segundo, le estás diciendo a JavaScript: "ejecuta esta tarea cada segundo".//intervalo = ...: setInterval te devuelve un "ID de identificación". Es como el número de serie de tu alarma. //Lo guardas en la variable intervalo para que, si algún día quieres apagarla, puedas decirle: "¡Hey, detén el intervalo número X!".return'Temporizador iniciado.';}return'El temporizador ya inició';},detener(){if(corriendo){ corriendo=false;clearInterval(intervalo);return'Se detuvo el temporizador'}return'el temporizador está apagado';},reiniciar(){ segundos=0;return'temporizador reiniciado';},obtenerTiempo(){return segundos;},};}const miTemporizador=crearTemporizador();console.log(miTemporizador.iniciar());console.log(miTemporizador.detener());console.log(miTemporizador.reiniciar());console.log(miTemporizador.obtenerTiempo());// --- Reto 7: Gestor de Tareas ---// Declara variables privadas:// let tareas = []// let idCounter = 1// Cada tarea debe tener esta estructura:// {// id,// tarea,// completada// }// Retorna un objeto con:// agregarTarea(tarea)// - crea una nueva tarea// - completada inicia en false// - el ID se incrementa automáticamente// retorna: `Tarea "${}" agregada con ID ${}.`// completarTarea(id)// - busca la tarea por ID// - pista: usa .find()// - cambia completada a true// obtenerTareas()// - retorna todas las tareas// tareasPendientes()// - retorna solo las tareas no completadas// pista: usa .filter()functioncrearGestorTareas(){let tareas =[];let idCounter =0;return{agregarTarea(tarea){//crea tarea y se añade a la lista tareas.push({id: idCounter++,tarea:tarea,completada:false,});return`${tarea} agregada con ID ${idCounter}.`;},completarTarea(id){//usa find para encontrar la tarea con el id provistoconst tarea = tareas.find((t)=> t.id=== id);//regresará la primera que encuentre o undefinedif(tarea){ tarea.completada=true;return`Tarea ${id} (${tarea.tarea}) cambiada a completada.`;;}return`ID ${id} no encontrado`;},obtenerTareas(){return tareas;},tareasPendientes(){return tareas.filter((t)=>!t.completada);;},};}const gestor =crearGestorTareas();console.log(gestor.agregarTarea("Estudiar"));console.log(gestor.agregarTarea("Comer"));console.log(gestor.completarTarea(1));console.log(gestor.obtenerTareas());console.log(gestor.tareasPendientes());// ============================================// Nivel 6 — Desafío Final// ============================================// --- Reto 8: Banco con Múltiples Cuentas ---// Declara variables privadas:// let cuentas = {}// let idCounter = 1// Cada cuenta debe crearse usando:// crearCuentaBancaria()// Métodos:// crearCuenta(saldoInicial)// - crea una nueva cuenta bancaria// - la guarda en el objeto cuentas// - incrementa el ID automáticamente// obtenerCuenta(id)// - retorna la cuenta con ese ID// - si no existe retorna null// eliminarCuenta(id)// - elimina la cuenta// - retorna:// "Cuenta ${id} eliminada."functioncrearCuentaBancaria(saldoInicial){// tu codigo aquílet saldo=saldoInicial;return{depositar(cantidad){ saldo+=cantidad;return`Se depositó $${cantidad}. Saldo actual $${saldo}`;},retirar(cantidad){if(cantidad<saldo){ saldo-=cantidad;return`Se retiró $${cantidad}. Saldo: $${saldo}`;}else{return`Fondos insuficiente`;}},consultarSaldo(){return`El saldo es $${saldo}`;}}}functioncrearBanco(){//variables privadas (closure)let cuentas ={};//guarda todas la cuentaslet idCounter =0;return{crearCuenta(saldoInicial){const id = idCounter++; cuentas[id]=crearCuentaBancaria(saldoInicial);//reutilizando la función pasadareturn`Cuenta ${id} creada con saldo inicial $${saldoInicial}.`;},obtenerCuenta(id){return cuentas[id]||null;//null evita que sea undefined},eliminarCuenta(id){if(cuentas[id]){delete cuentas[id];return`Cuenta ${id} eliminada.`;}return`Cuenta con ID ${id} no encontrada.`;},};}const banco =crearBanco();console.log(banco.crearCuenta(1000));// Cuenta 0console.log(banco.crearCuenta(500));// Cuenta 1const cuenta0 = banco.obtenerCuenta(0);console.log(cuenta0);console.log(banco.eliminarCuenta(1));console.log(cuenta0.consultarSaldo());console.log(cuenta0.depositar(200));console.log(cuenta0.retirar(500));module.exports={ crearContador, crearAcumulador, crearCuentaBancaria, crearCache, crearCarrito, crearTemporizador, crearGestorTareas, crearBanco,};
Para esta clase el reto 15-closures-javascript ya estaba resuelto ;( O tal vez no entendí qué es lo que se debía hacer.
De todas formas hice mi propio reto con un sistema de gestión de inventario.
// Sistema de gestión de inventariofunctiongestionarInventario(unidades){let totalDeUnidades = unidades;functionesUnaCantidadValida(cantidad){if(!Number.isInteger(cantidad)|| cantidad ===0|| cantidad <0){returnfalse;}else{returntrue;}}return{agregarStock(cantidad){if(esUnaCantidadValida(cantidad)){ totalDeUnidades += cantidad;if(cantidad ===1){return'Se ha agregado 1 unidad';}else{return`Se han agregado ${cantidad} unidades`;}}else{return'No enviaste una cantidad válida';}},venderProductos(cantidad){if(esUnaCantidadValida(cantidad)){if(cantidad > totalDeUnidades){return'No hay suficiente inventario para completar el pedido';}else{ totalDeUnidades -= cantidad;if(cantidad ===1){return'Ha salido una unidad del inventario';}elseif(cantidad >1){return`Han salido ${cantidad} unidades del inventario`;}}}else{return'No enviaste una cantidad válida';}},consultarInventario(){return totalDeUnidades;}}}const miInventario =gestionarInventario(100);console.log(miInventario.venderProductos(10));console.log(miInventario.consultarInventario());console.log(miInventario.agregarStock(5));console.log(miInventario.consultarInventario());
cuál es la diferencia entre crear un objeto o una clase y el uso de Closures o van de la mano ?
Sergio Andres, un closure es una función que "recuerda" el scope en el que fue creada, incluso después de que esa función externa haya terminado de ejecutarse.
La diferencia principal con crear un objeto o una clase es que los closures permiten crear variables privadas (como el saldo en el ejemplo de la cuenta bancaria) que solo son accesibles a través de las funciones internas retornadas. Los objetos y clases, si bien pueden encapsular datos, no crean este tipo de "estado privado" de la misma manera que un closure. Los closures y los objetos/clases pueden trabajar juntos, pero el closure es el mecanismo que permite que las funciones internas mantengan acceso a variables de su scope de creación.