Introduzione
In JavaScript, le funzioni sono cittadini di prima classe: possono essere assegnate a variabili, passate come argomenti, restituite da altre funzioni e salvate in strutture dati. Questo aspetto rende il linguaggio estremamente flessibile e permette di costruire API eleganti, modulari e riutilizzabili.
Un concetto strettamente collegato è quello di closure. Una closure nasce quando una funzione “ricorda” l’ambiente lessicale in cui è stata creata, anche dopo che la funzione esterna ha terminato l’esecuzione. In pratica, una funzione interna può continuare ad accedere a variabili definite nel contesto esterno.
In questo tutorial vediamo un sotto-argomento pratico e molto utile: usare closures e funzioni di prima classe per creare factory di validazione riutilizzabili. È un approccio perfetto per costruire validatori personalizzati, ad esempio per email, password, range numerici o form complessi, evitando codice duplicato e rendendo il progetto più mantenibile.
Codice completo
// Factory che crea un validatore generico
function createValidator(rules) {
return function validate(value) {
const errors = [];
// Regola: valore obbligatorio
if (rules.required && (value === null || value === undefined || value === "")) {
errors.push("Il valore è obbligatorio.");
}
// Regola: tipo stringa
if (rules.type === "string" && typeof value !== "string") {
errors.push("Il valore deve essere una stringa.");
}
// Regola: lunghezza minima
if (typeof value === "string" && rules.minLength !== undefined) {
if (value.length rules.maxLength) {
errors.push(`La lunghezza massima è ${rules.maxLength} caratteri.`);
}
}
// Regola: pattern regex
if (rules.pattern && typeof value === "string") {
if (!rules.pattern.test(value)) {
errors.push("Il formato non è valido.");
}
}
// Regola: range numerico
if (typeof value === "number") {
if (rules.min !== undefined && value rules.max) {
errors.push(`Il valore deve essere minore o uguale a ${rules.max}.`);
}
}
return {
valid: errors.length === 0,
errors
};
};
}
// Creiamo validatori specializzati riusando la stessa factory
const validateUsername = createValidator({
required: true,
type: "string",
minLength: 3,
maxLength: 15,
pattern: /^[a-zA-Z0-9_]+$/
});
const validatePassword = createValidator({
required: true,
type: "string",
minLength: 8,
pattern: /^(?=.*[A-Z])(?=.*[a-z])(?=.*d).+$/
});
const validateAge = createValidator({
required: true,
type: "number",
min: 18,
max: 99
});
// Esempi di utilizzo
console.log(validateUsername("mario_rossi"));
// { valid: true, errors: [] }
console.log(validateUsername("m!"));
// { valid: false, errors: [ ´La lunghezza minima è 3 caratteri.´, ´Il formato non è valido.´ ] }
console.log(validatePassword("Password1"));
// { valid: true, errors: [] }
console.log(validatePassword("abc"));
// { valid: false, errors: [ ´La lunghezza minima è 8 caratteri.´, ´Il formato non è valido.´ ] }
console.log(validateAge(20));
// { valid: true, errors: [] }
console.log(validateAge(15));
// { valid: false, errors: [ ´Il valore deve essere maggiore o uguale a 18.´ ] }
// Esempio di composizione: più validatori su un campo
function combineValidators(...validators) {
return function (value) {
const allErrors = [];
for (const validator of validators) {
const result = validator(value);
if (!result.valid) {
allErrors.push(...result.errors);
}
}
return {
valid: allErrors.length === 0,
errors: allErrors
};
};
}
const validateStrongUsername = combineValidators(
validateUsername,
createValidator({
pattern: /^[a-z]/ // deve iniziare con una lettera minuscola
})
);
console.log(validateStrongUsername("Mario_1"));
// { valid: false, errors: [ ´Il formato non è valido.´ ] } Spiegazione
Il cuore dell’esempio è la funzione createValidator. Questa funzione prende un oggetto di configurazione rules e restituisce una nuova funzione validate. Qui entra in gioco la closure: la funzione restituita continua ad accedere a rules anche dopo che createValidator ha finito di eseguire.
Perché è utile
Invece di scrivere una funzione diversa per ogni tipo di validazione, creiamo una factory, cioè una funzione che genera funzioni specializzate. Questo è possibile perché le funzioni in JavaScript sono valori, quindi possono essere:
- salvate in variabili;
- restituite da altre funzioni;
- passate come parametri;
- combinate tra loro per costruire logiche più complesse.
Come funziona la closure
Quando scriviamo:
const validateUsername = createValidator({
required: true,
type: "string",
minLength: 3
}); stiamo ottenendo una funzione che “si ricorda” delle regole passate in quel momento. Anche se createValidator termina, le regole restano disponibili dentro validate. Questo è il comportamento tipico delle closure.
Validazione come esempio reale
La validazione è un caso d’uso molto comune nei progetti reali: form di registrazione, checkout e gestione profili. Con questo approccio possiamo creare regole riutilizzabili e coerenti. Ad esempio, un validatore per username e uno per password condividono la stessa struttura, ma con regole diverse.
Questo porta vantaggi concreti:
- meno duplicazione del codice;
- maggiore leggibilità;
- manutenzione più semplice;
- test più facili, perché ogni validatore è una funzione indipendente.
Composizione di funzioni
La funzione combineValidators mostra un altro punto importante: le funzioni come cittadini di prima classe permettono di comporre comportamenti. Qui passiamo più validatori a una funzione che restituisce un nuovo validatore combinato.
Questo stile è molto utile quando vuoi separare le responsabilità. Ogni validatore si occupa di una sola regola, mentre la funzione combinatrice gestisce il risultato finale.
Best practice
- Preferisci factory piccole e focalizzate: una funzione che crea validatori dovrebbe avere una responsabilità chiara.
- Evita effetti collaterali: il validatore dovrebbe limitarsi a controllare il valore e restituire un risultato, senza modificare dati esterni.
- Restituisci strutture coerenti: un oggetto con valid ed errors è facile da usare e da testare.
- Usa regex con attenzione: i pattern sono potenti, ma devono essere leggibili e ben documentati.
- Componi funzioni invece di creare logiche monolitiche: più il codice è modulare, più è semplice da estendere.
- Scrivi validatori riutilizzabili: se una regola compare in più punti, trasformala in una funzione dedicata.
Un consiglio pratico: quando una closure cattura dati di configurazione, assicurati che quei dati siano stabili e non vengano modificati in modo inatteso. In caso contrario, il comportamento della funzione può diventare difficile da prevedere.
Riepilogo
Le closures e le funzioni come cittadini di prima classe sono due pilastri fondamentali di JavaScript. In questo tutorial abbiamo visto come usarli per costruire factory di validazione riutilizzabili, un approccio molto utile nei progetti reali.
- Una closure permette a una funzione interna di ricordare il contesto esterno.
- Le funzioni di prima classe consentono di creare, passare e restituire funzioni con grande flessibilità.
- Le factory permettono di generare validatori specializzati partendo da una base comune.
- La composizione di funzioni rende il codice più pulito e riutilizzabile.
Se vuoi scrivere JavaScript moderno e ben strutturato, padroneggiare questi concetti è essenziale. Ti aiuteranno a progettare API più eleganti, a ridurre la duplicazione e a costruire codice più facile da mantenere nel tempo.
Approfondisci con risorse ufficiali
- MDN Web Docs - Closures: documentazione chiara e completa sul funzionamento delle closure in JavaScript.
- MDN Web Docs - Functions: guida ufficiale alle funzioni, ai parametri e al comportamento del linguaggio.
- MDN Web Docs - Expressions and operators: utile per approfondire regex, confronti e logica condizionale.
- ECMAScript Language Specification: la specifica ufficiale del linguaggio per chi vuole andare più a fondo.
