Closures en JavaScript: validación de formularios y estado interno sin frameworks

Si llevas tiempo programando en JavaScript, sabrás que los closures son una herramienta poderosa, pero a veces resulta difícil ver cuándo aplicarlos en proyectos reales. En este artículo vamos a centrarnos en un caso concreto que muchos desarrolladores front‑end encuentran a diario: la validación de formularios. Veremos cómo un closure nos permite encapsular la lógica de validación, mantener el estado interno y reutilizar el mismo código en varios formularios sin necesidad de frameworks.
¿Qué es un closure?
Un closure es simplemente una función que recuerda el entorno léxico en el que se creó, es decir, conserva acceso a variables que estaban declaradas fuera de ella aunque el contexto externo haya finalizado. En JavaScript esto ocurre de forma automática cuando una función interna hace referencia a una variable de la función externa.
Ventajas de usar closures para validar formularios
- Encapsulamiento: la lógica de validación queda aislada y no contamina el ámbito global.
- Estado interno: podemos guardar datos como el número de intentos, mensajes de error o configuraciones sin usar variables globales.
- Reutilización: el mismo validador puede aplicarse a varios formularios con distinta configuración.
- Facilidad de testing: al estar todo contenido en una única función, resulta sencillo escribir pruebas unitarias.
Ejemplo práctico: un validador configurable
Imagina que tienes varios formularios de registro, contacto y suscripción. Todos comparten reglas básicas (campo obligatorio, email válido) pero cada uno necesita validaciones específicas. Crearemos una fábrica de validadores que devuelve una función de validación personalizada.
function crearValidador(opciones) {
// Variables que permanecerán en el closure
const errores = [];
const { requerido = true, minLength = 0, patron = null } = opciones;
// La función que realmente valida
return function validar(campo, valor) {
// Reiniciamos errores en cada llamada
errores.length = 0;
if (requerido && (!valor || valor.trim() === "")) {
errores.push(`${campo} es obligatorio`);
}
if (valor && valor.length < minLength) {
errores.push(`${campo} debe tener al menos ${minLength} caracteres`);
}
if (patron && !patron.test(valor)) {
errores.push(`${campo} no cumple el formato esperado`);
}
// Devolvemos un objeto con el resultado y los mensajes
return {
valido: errores.length === 0,
mensajes: [...errores]
};
};
}
// Uso en diferentes formularios
const validarEmail = crearValidador({
patron: /^[^@\s]+@[^@\s]+\.[^@\s]+$/
});
const validarPassword = crearValidador({
minLength: 8,
patron: /(?=.*[A-Z])(?=.*\d)/
});
// Simulación de envío
function enviar(formulario) {
const resultadoEmail = validarEmail('Email', formulario.email);
const resultadoPass = validarPassword('Password', formulario.password);
if (!resultadoEmail.valido || !resultadoPass.valido) {
console.log('Errores de validación:', {
email: resultadoEmail.mensajes,
password: resultadoPass.mensajes
});
return false;
}
console.log('Formulario válido, enviando…');
// Aquí iría la llamada a fetch/AJAX
return true;
}
// Prueba rápida
enviar({ email: 'test@dominio', password: 'abc' });
En este fragmento, crearValidador devuelve una función validar que tiene acceso a errores y a la configuración recibida (requerido, minLength, patron). Cada vez que llamamos a validar se crea un nuevo array de errores, pero la configuración permanece constante gracias al closure.
Desglose paso a paso
- Configuración inicial: al llamar a
crearValidadordefinimos qué reglas aplicará el validador. Esta información queda “capturada” por el closure. - Estado interno: la variable
erroresestá dentro del scope externo y se reutiliza en cada llamada, garantizando que los mensajes anteriores no se mezclen con los nuevos. - Retorno estructurado: devolvemos un objeto con
validoymensajes, lo que hace que el código que consuma el validador sea muy legible.
Integración con el DOM
En un proyecto real, lo más habitual es conectar este validador a los eventos submit o input de los formularios. Aquí tienes un ejemplo sencillo:
document.querySelector('#formRegistro').addEventListener('submit', function(e) {
e.preventDefault();
const data = {
email: this.email.value,
password: this.password.value
};
if (enviar(data)) {
this.reset();
}
});
Al separar la lógica de validación del manejo del DOM, conseguimos código modular y fácil de testear. Además, si en el futuro necesitas añadir una regla (por ejemplo, validar un número de teléfono), solo tendrás que crear otro validador con la configuración adecuada.
Buenas prácticas y trampas comunes
- No abusar de los closures para todo: si la lógica es trivial, envolverla en un closure puede añadir complejidad innecesaria.
- Evita mutar variables externas sin motivo: en el ejemplo usamos
errores.length = 0para vaciar el array, pero podrías crear uno nuevo en cada llamada para mayor claridad. - Ten cuidado con los patrones RegExp: los objetos RegExp son mutables; si los modificas dentro del closure, afectarás a otras instancias.
- Documenta la configuración: cuando tu validador acepte opciones, incluye comentarios o un fichero de tipos (si usas TypeScript) para que otros devs sepan qué parámetros pueden pasar.
Conclusión
Los closures son mucho más que un concepto teórico; son una solución práctica para encapsular estado y reutilizar lógica en la vida real. En el caso de la validación de formularios, nos permiten crear validadores configurables, mantener los errores aislados y evitar la contaminación del ámbito global. La próxima vez que tengas que validar varios formularios, prueba a crear una fábrica de validadores con closures: ahorrarás tiempo, tendrás código más limpio y, lo más importante, mejorarás la mantenibilidad de tu proyecto.