Introduzione
In JavaScript, le funzioni sono cittadini di prima classe: possono essere assegnate a variabili, passate come argomenti, restituite da altre funzioni e memorizzate in strutture dati. Questa caratteristica, unita alle closures, permette di costruire soluzioni eleganti e molto pratiche senza dover ricorrere a classi complesse o dipendenze pesanti.
In questo tutorial vedremo un caso d’uso molto utile e spesso sottovalutato: creare un sistema di eventi leggero e riutilizzabile, simile a un mini event emitter. È un ottimo esempio perché sfrutta entrambe le idee fondamentali:
- funzioni come valori, per registrare callback dinamiche;
- closures, per mantenere privato lo stato interno del sistema.
Un sistema del genere è utile in piccoli progetti, librerie custom, componenti frontend, script Node.js e in generale quando vuoi coordinare più parti di codice in modo disaccoppiato.
Codice completo
// Factory che crea un event emitter leggero
function createEventBus() {
// Stato privato: non è accessibile dall´esterno
const listeners = new Map();
function on(eventName, callback) {
if (typeof callback !== "function") {
throw new TypeError("callback deve essere una funzione");
}
if (!listeners.has(eventName)) {
listeners.set(eventName, new Set());
}
listeners.get(eventName).add(callback);
// Restituiamo una funzione di cleanup
// grazie alla closure conserva accesso a eventName e callback
return function off() {
const set = listeners.get(eventName);
if (!set) return;
set.delete(callback);
if (set.size === 0) {
listeners.delete(eventName);
}
};
}
function emit(eventName, payload) {
const set = listeners.get(eventName);
if (!set) return;
// Copia difensiva per evitare problemi se un listener si rimuove durante l´emissione
[...set].forEach((callback) => {
callback(payload);
});
}
function once(eventName, callback) {
if (typeof callback !== "function") {
throw new TypeError("callback deve essere una funzione");
}
const off = on(eventName, (payload) => {
off(); // si deregistra dopo la prima esecuzione
callback(payload);
});
return off;
}
function clear(eventName) {
if (typeof eventName === "undefined") {
listeners.clear();
return;
}
listeners.delete(eventName);
}
function count(eventName) {
const set = listeners.get(eventName);
return set ? set.size : 0;
}
// Esponiamo solo l´API pubblica
return {
on,
once,
emit,
clear,
count
};
}
// --------------------
// ESEMPIO DI UTILIZZO
// --------------------
const bus = createEventBus();
const stopUserCreated = bus.on("user:created", (user) => {
console.log("Nuovo utente:", user.name);
});
bus.once("user:created", (user) => {
console.log("Questo messaggio appare una sola volta per:", user.name);
});
bus.emit("user:created", { name: "Luca" });
// Output:
// Nuovo utente: Luca
// Questo messaggio appare una sola volta per: Luca
bus.emit("user:created", { name: "Sara" });
// Output:
// Nuovo utente: Sara
console.log("Listener attivi:", bus.count("user:created")); // 1
stopUserCreated(); // rimuove il listener manualmente
bus.emit("user:created", { name: "Marta" });
// Nessun output
bus.clear(); // rimuove tutto
console.log("Listener attivi:", bus.count("user:created")); // 0 Spiegazione
Il cuore dell’esempio è la funzione createEventBus(). Questa funzione non esegue un’azione immediata: costruisce e restituisce un oggetto API che contiene metodi per registrare, emettere e rimuovere eventi. Qui entrano in gioco le closures.
1. Lo stato privato con la closure
La variabile listeners è dichiarata dentro createEventBus. Non viene restituita direttamente, quindi dall’esterno non è accessibile. Tuttavia, i metodi on, emit, once, clear e count la “ricordano” perché sono stati creati nello stesso scope. Questa è una closure: una funzione che conserva accesso alle variabili del contesto in cui è nata, anche dopo che la funzione esterna ha terminato l’esecuzione.
2. Le funzioni come cittadini di prima classe
Nel sistema eventi, le callback sono passate come argomenti a on e once. Questo è possibile perché in JavaScript le funzioni sono valori a tutti gli effetti. Possiamo:
- salvarle in un
Set; - passarle da una parte all’altra del programma;
- restituirle da una factory, come nel caso della funzione
off; - eseguirle in un secondo momento, quando avviene l’evento.
3. Perché usare Set e Map
Abbiamo scelto Map per associare ogni nome evento al suo insieme di listener, e Set per evitare duplicati. È una scelta pratica perché:
- l’accesso agli eventi è veloce;
- la rimozione di un listener è semplice;
- la struttura del codice rimane leggibile.
4. La funzione once come esempio di composizione
La funzione once dimostra bene il potere delle closures. Registra un listener tramite on, ma crea anche una funzione di rimozione (off) che viene richiamata al primo evento. In questo modo il callback viene eseguito una sola volta senza bisogno di logica esterna complicata.
5. Cleanup esplicito
Restituire una funzione di cleanup è una best practice importante. Nel nostro caso on restituisce off, così chi usa l’API può rimuovere facilmente il listener quando non serve più. Questo approccio è molto utile in frontend, ad esempio quando un componente viene smontato e bisogna evitare memory leak.
Best practice
- Valida sempre i callback: controllare che l’argomento sia una funzione evita errori difficili da diagnosticare.
- Nascondi lo stato interno: la closure è utile proprio per proteggere i dati privati e limitare l’API pubblica.
- Restituisci funzioni di cleanup: rendono più semplice la gestione del ciclo di vita dei listener.
- Evita effetti collaterali durante l’emissione: copiare l’insieme dei listener con
[...set]previene comportamenti imprevedibili se un listener si deregistra mentre gli eventi vengono emessi. - Usa nomi evento consistenti: una convenzione come
module:actionodomain:eventaiuta a mantenere il sistema leggibile. - Non abusare delle closures: sono potenti, ma se il codice cresce troppo può diventare difficile da debuggare. Mantieni l’API piccola e chiara.
Riepilogo
Le closures e le funzioni come cittadini di prima classe sono due pilastri fondamentali di JavaScript. In questo tutorial li abbiamo usati per creare un mini event bus con stato privato, listener registrabili, eventi one-shot e cleanup esplicito.
Questo pattern è pratico perché:
- separa chi emette gli eventi da chi li ascolta;
- mantiene il codice modulare;
- permette di incapsulare lo stato senza classi;
- mostra un uso reale e professionale delle closures.
Se vuoi migliorare ancora questo esempio, puoi aggiungere supporto per listener asincroni, priorità di esecuzione, namespace degli eventi o log di debug. Ogni estensione continuerà a beneficiare della stessa idea di base: una factory che crea oggetti con stato privato e funzioni pubbliche riutilizzabili.
Approfondisci con risorse ufficiali
- MDN - Closures: documentazione completa sulle closure in JavaScript.
- MDN - Functions: panoramica sulle funzioni e sul loro comportamento come valori.
- MDN - Map: guida all’uso della struttura dati
Map. - MDN - Set: documentazione sulla collezione
Set. - ECMAScript Language Specification: riferimento ufficiale per il comportamento del linguaggio.
