Una closure è una funzione che ricorda
Ogni volta che definisci una funzione in JavaScript, questa mantiene in silenzio un collegamento alle variabili che la circondano. Quando la funzione viene eseguita più tardi, anche in un punto completamente diverso, può ancora vedere quelle variabili. Questa è una closure.
La dimostrazione più breve:
makeGreeter viene eseguita, restituisce una funzione interna ed esce. Ti aspetteresti che la sua variabile locale name sparisca, visto che la funzione ha finito. Ma la funzione interna usa ancora name, quindi JavaScript la tiene in vita. greetAda ricorda "Ada". greetBoris ricorda "Boris". Due closure, due valori ricordati separatamente.
Le closure nascono dallo scope lessicale
La regola dietro le closure si chiama scope lessicale: una funzione vede le variabili del punto in cui è stata scritta, non del punto in cui viene chiamata. "Lessicale" significa solo "in base alla sua posizione nel codice sorgente".
show stampa "sono fuori", non "sono dentro caller". È stata scritta accanto alla outer di primo livello, quindi è quella che vede. Chiamarla da un punto che ha una propria outer non cambia nulla.
Le closure sono semplicemente scope lessicale che sopravvive alla funzione esterna. La variabile non sparisce perché qualcuno ha ancora un riferimento a essa.
Ogni chiamata ha la sua closure
Una nuova chiamata alla funzione esterna crea nuove variabili, e ogni funzione interna restituita da quella chiamata ricorda quelle variabili. Ecco perché greetAda e greetBoris qui sopra non si sono pestate i piedi.
L'esempio classico è un contatore:
a e b hanno ciascuno il proprio count. Niente al di fuori della funzione restituita può toccare quelle variabili: count è completamente privato. Non è una funzionalità del linguaggio che abbiamo attivato: è una conseguenza di come funzionano le closure.
Variabili private senza classi
Dato che le variabili racchiuse sono raggiungibili solo attraverso la funzione restituita, puoi usare le closure per costruire piccoli oggetti con uno stato davvero privato:
balance non è una proprietà dell'oggetto restituito: vive nella closure. L'unico modo per leggerlo o modificarlo è passare dai metodi che hai esposto. Anche le classi con i campi #private possono farlo, ma la versione con le closure li precede di decenni e compare ancora ovunque nell'ecosistema.
La classica trappola dei cicli
Le closure mettono in difficoltà soprattutto dentro i cicli. Guarda cosa succede con var:
Ti aspetteresti 0, 1, 2. Ottieni 3, 3, 3. Ecco perché: var ha scope di funzione, quindi esiste una sola i per tutto il ciclo. Le tre closure hanno catturato la stessa variabile, e quando vengono eseguite il ciclo è finito e i vale 3.
Passa a let:
Ora stampa 0, 1, 2. let ha scope di blocco: ogni iterazione del ciclo crea un nuovo binding per i, quindi ogni closure cattura il proprio valore. È il motivo principale per preferire let a var.
Le closure catturano variabili, non valori
Un punto sottile ma importante: una closure si tiene la variabile, non una fotografia del suo valore al momento in cui la funzione è stata definita.
printMessage legge message quando viene eseguita, non quando è stata creata. Se vuoi una fotografia, copia prima il valore in una variabile locale: in pratica è quello che fa let dentro un ciclo for.
Un pattern reale molto comune: once
Ecco una piccola utility che usa una closure per assicurarsi che una funzione venga eseguita una sola volta:
called e result sono uno stato privato che vive quanto la funzione restituita. Nessun flag globale, nessun oggetto in più. Questo pattern (piccola funzione di supporto, stato privato, closure) è una delle cose più utili che JavaScript sa fare.
Due parole sulla memoria
Una closure mantiene in vita le variabili catturate finché qualcosa fa ancora riferimento alla closure. Di solito è quello che vuoi, ma se colleghi una closure a qualcosa di longevo (come un event listener del DOM o una cache globale) e questa cattura qualcosa di grande, quella cosa non può essere liberata dal garbage collector finché la closure non sparisce.
function attach() {
const hugeData = new Array(1_000_000).fill("...");
document.addEventListener("click", () => {
console.log(hugeData.length);
});
}
Finché il listener è collegato, hugeData resta in memoria. Rimuovi il listener (o evita di catturare ciò che non ti serve) e il riferimento si libera. Non devi gestire tutto questo nei minimi dettagli: basta sapere che closure e memoria sono collegate.
Cosa portarti a casa
- Una closure è una funzione più le variabili che vedeva quando è stata definita.
- Ogni chiamata a una funzione esterna crea un nuovo insieme di variabili per le sue closure interne.
- Le closure ti danno uno stato privato senza bisogno di classi.
- Dentro i cicli usa
let, così ogni iterazione ha il proprio binding. - Le closure catturano la variabile stessa, non il valore al momento della creazione.
Prossimo passo: la parola chiave this
Le closure gestiscono le variabili attorno a una funzione. Il prossimo tassello è l'oggetto su cui viene chiamata una funzione, che in JavaScript è controllato da this e si comporta in modo molto diverso dalle variabili catturate che abbiamo appena visto.
Domande frequenti
Cos'è una closure in JavaScript?
Una closure è una funzione che ricorda le variabili dello scope in cui è stata definita, anche dopo che quello scope esterno ha finito di essere eseguito. Tecnicamente in JavaScript ogni funzione è una closure: il termine si usa di solito quando una funzione viene restituita o passata altrove e continua a usare le variabili del suo scope originale.
A cosa servono le closure?
Permettono a una funzione di portarsi dietro uno stato privato. Ottieni dati legati a una funzione senza bisogno di una classe o di una variabile globale. Gli usi più comuni sono contatori, callback da eseguire una sola volta, funzioni con memoizzazione e nascondere i dettagli di implementazione dietro una piccola API.
Perché le closure si comportano in modo strano nei cicli con var?
var ha scope di funzione, quindi tutte le iterazioni condividono la stessa variabile. Le closure create nel ciclo fanno tutte riferimento a quell'unica variabile, che quando vengono eseguite ha già il valore finale. Usa invece let: ha scope di blocco, quindi ogni iterazione riceve il proprio binding.