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. Questa caratteristica apre la porta a tecniche molto potenti, tra cui le closures, cioè funzioni che “ricordano” il contesto in cui sono state create.
Un uso molto pratico di questo meccanismo è la creazione di decoratori di funzione: funzioni che prendono in input un’altra funzione e la arricchiscono con comportamento aggiuntivo, senza modificarne il codice originale. È un approccio utile per logging, misurazione delle prestazioni, gestione errori, retry e molto altro.
In questo tutorial vedremo come costruire un decoratore riutilizzabile per aggiungere tracciamento del tempo di esecuzione e gestione degli errori a qualsiasi funzione. È un caso d’uso concreto, molto comune in applicazioni reali e perfetto per capire come si combinano closures e funzioni come cittadini di prima classe.
Codice completo
// Decoratore generico: prende una funzione e restituisce una nuova funzione
// che aggiunge logging, timing e gestione errori.
function withMonitoring(fn, label = fn.name || "anonymous") {
// La closure conserva label e fn anche dopo la fine dell´esecuzione di withMonitoring
return function (...args) {
const start = performance.now();
try {
console.log(`[${label}] Avvio con argomenti:`, args);
const result = fn(...args);
// Se la funzione restituisce una Promise, gestiamola in modo asincrono
if (result instanceof Promise) {
return result
.then((value) => {
const end = performance.now();
console.log(`[${label}] Completata in ${(end - start).toFixed(2)} ms`);
return value;
})
.catch((error) => {
const end = performance.now();
console.error(`[${label}] Errore dopo ${(end - start).toFixed(2)} ms:`, error);
throw error;
});
}
const end = performance.now();
console.log(`[${label}] Completata in ${(end - start).toFixed(2)} ms`);
return result;
} catch (error) {
const end = performance.now();
console.error(`[${label}] Errore dopo ${(end - start).toFixed(2)} ms:`, error);
throw error;
}
};
}
// Funzione sincrona di esempio
function sommaLenta(a, b) {
for (let i = 0; i < 5e6; i++) {
// Simula lavoro pesante
}
return a + b;
}
// Funzione asincrona di esempio
async function recuperaUtente(id) {
if (id <= 0) {
throw new Error("ID non valido");
}
// Simulazione di chiamata API
await new Promise((resolve) => setTimeout(resolve, 500));
return { id, nome: "Mario Rossi" };
}
// Creiamo versioni decorate
const sommaMonitorata = withMonitoring(sommaLenta, "sommaLenta");
const recuperaUtenteMonitorata = withMonitoring(recuperaUtente, "recuperaUtente");
// Uso della funzione decorata
console.log("Risultato somma:", sommaMonitorata(10, 20));
recuperaUtenteMonitorata(1)
.then((utente) => console.log("Utente:", utente))
.catch((err) => console.error("Gestito in chiamata:", err.message));
recuperaUtenteMonitorata(-1)
.catch((err) => console.error("Errore atteso:", err.message)); Spiegazione
Il cuore dell’esempio è la funzione withMonitoring. Questa funzione riceve un’altra funzione, fn, e restituisce una nuova funzione che esegue il comportamento originale aggiungendo funzionalità extra.
1. Funzioni come valori
In JavaScript una funzione può essere trattata come qualsiasi altro valore:
- può essere salvata in una variabile;
- può essere passata come argomento;
- può essere restituita da un’altra funzione.
Nel nostro caso, fn viene passata a withMonitoring. Questo è possibile proprio perché le funzioni sono cittadini di prima classe.
2. La closure che conserva lo stato
La funzione restituita da withMonitoring usa due variabili definite all’esterno: fn e label. Anche quando withMonitoring ha terminato la sua esecuzione, la funzione interna continua ad avere accesso a quei valori. Questa è una closure.
Grazie alla closure, ogni funzione decorata conserva il proprio contesto:
- la funzione originale da eseguire;
- l’etichetta da stampare nei log;
- la logica di misurazione già configurata.
3. Supporto per funzioni sincrone e asincrone
Un dettaglio importante è la gestione di entrambi i casi:
- se fn restituisce un valore normale, il decoratore misura il tempo e restituisce il risultato;
- se fn restituisce una Promise, il decoratore aspetta la risoluzione o il fallimento e registra il tempo corretto.
Questo rende il decoratore molto più utile in progetti reali, dove le funzioni asincrone sono la norma, soprattutto quando si lavora con API, database o filesystem.
4. Riutilizzo senza duplicazione
Il vantaggio principale è la separazione delle responsabilità. La funzione sommaLenta si occupa solo di sommare. La funzione recuperaUtente si occupa solo di recuperare dati. Il logging e il timing vengono aggiunti dall’esterno, senza sporcare la logica di business.
Questo approccio è molto più pulito rispetto a copiare e incollare blocchi di console.log e misurazioni in ogni funzione del progetto.
Best practice
- Non modificare la funzione originale se puoi evitarlo: preferisci decorarla e restituire una nuova funzione.
- Mantieni il decoratore generico: dovrebbe funzionare con più funzioni possibili, non solo con un caso specifico.
- Gestisci sia sync che async: oggi molte funzioni possono restituire Promise, quindi il decoratore deve essere robusto.
- Usa nomi chiari:
withMonitoring,withRetry,withCachesono nomi leggibili e descrittivi. - Non abusare dei side effect: il logging va bene, ma evita di introdurre comportamenti nascosti difficili da testare.
- Preserva il comportamento originale: se la funzione lancia un errore, il decoratore deve rilanciarlo dopo averlo gestito o registrato.
- Valuta i casi limite: funzioni che non restituiscono nulla, funzioni che lanciano eccezioni, funzioni asincrone che falliscono.
Un’estensione molto utile di questo pattern è creare altri decoratori, ad esempio:
- un decoratore per fare retry automatici;
- un decoratore per aggiungere cache;
- un decoratore per limitare la frequenza di chiamata con throttle o debounce;
- un decoratore per validare gli argomenti prima dell’esecuzione.
Riepilogo
Le closures e le funzioni come cittadini di prima classe sono due concetti fondamentali di JavaScript che lavorano benissimo insieme. In questo tutorial abbiamo visto come usarli per costruire un decoratore riutilizzabile che aggiunge monitoraggio, timing e gestione errori a una funzione esistente.
I punti chiave sono:
- le funzioni possono essere passate e restituite come valori;
- una closure conserva l’accesso alle variabili esterne;
- i decoratori aiutano a separare la logica principale dai comportamenti trasversali;
- un buon decoratore deve essere generico, robusto e compatibile con funzioni sincrone e asincrone.
Se vuoi scrivere codice più pulito e modulare, imparare a combinare closures e funzioni di ordine superiore è uno dei passi più importanti in JavaScript.
Approfondisci con risorse ufficiali
- MDN Web Docs - Closures: documentazione chiara e completa sul concetto di closure in JavaScript.
- MDN Web Docs - Functions: panoramica sulle funzioni, sui parametri e sul comportamento delle funzioni in JS.
- MDN Web Docs - Promise: utile per comprendere la gestione delle funzioni asincrone nel decoratore.
- MDN Web Docs - performance.now(): riferimento per misurare tempi con maggiore precisione rispetto a
Date.now(). - JavaScript.info - Function basics e Closures: risorse didattiche ben strutturate per consolidare le basi.
