Le callback, i gestori di eventi, le funzioni di ordine superiore possono accedere a variabili di ambito esterno grazie alle chiusure. Le chiusure sono importanti nella programmazione funzionale, e vengono spesso chieste durante il colloquio di codifica JavaScript.
Anche se sono usate ovunque, le chiusure sono difficili da capire. Se non avete ancora avuto il vostro momento “Aha!” nella comprensione delle chiusure, allora questo post è per voi.
Inizio con i termini fondamentali: scope e scope lessicale. Poi, dopo aver afferrato le basi, vi basterà un solo passo per capire finalmente le chiusure.
Prima di iniziare, vi suggerisco di resistere all’impulso di saltare le sezioni sullo scope e sullo scope lessicale. Questi concetti sono cruciali per le chiusure, e se li capite bene, l’idea di chiusura diventa evidente.
1. Lo scope
Quando si definisce una variabile, la si vuole accessibile entro certi limiti. Per esempio, una variabile result
ha senso che esista all’interno di una funzione calculate()
, come dettaglio interno. Al di fuori del calculate()
, la variabile result
è inutile.
L’accessibilità delle variabili è gestita dallo scope. Siete liberi di accedere alla variabile definita all’interno del suo ambito. Ma al di fuori di tale ambito, la variabile è inaccessibile.
In JavaScript, uno scope è creato da una funzione o da un blocco di codice.
Vediamo come lo scope influenza la disponibilità di una variabile count
. Questa variabile appartiene a uno scope creato dalla funzione foo()
:
function foo() { // The function scope let count = 0; console.log(count); // logs 0}foo();console.log(count); // ReferenceError: count is not defined
count
è liberamente accessibile nello scope di foo()
.
Tuttavia, al di fuori dello scopo foo()
count
è inaccessibile. Se si cerca di accedere a count
dall’esterno, JavaScript lancia ReferenceError: count is not defined
.
In JavaScript, l’ambito dice: se avete definito una variabile all’interno di una funzione o di un blocco di codice, allora potete usare questa variabile solo all’interno di quella funzione o blocco di codice. L’esempio qui sopra dimostra questo comportamento.
Ora, vediamo una formulazione generale:
Lo scope è una politica di spazio che regola l’accessibilità delle variabili.
Sorge una proprietà immediata: lo scope isola le variabili. Questo è ottimo perché scope diversi possono avere variabili con lo stesso nome.
Si possono riutilizzare nomi di variabili comuni (count
index
current
value
, etc) in diversi scope senza collisioni.
foo()
e bar()
gli scopi delle funzioni hanno le proprie variabili, ma con lo stesso nome count
:
function foo() { // "foo" function scope let count = 0; console.log(count); // logs 0}function bar() { // "bar" function scope let count = 1; console.log(count); // logs 1}foo();bar();
count
variabili da foo()
e bar()
function scope non collidono.
2. Annidamento degli scope
Giochiamo ancora un po’ con gli scope, e mettiamo uno scope in un altro.
La funzione innerFunc()
è annidata all’interno di una funzione esterna outerFunc()
.
Come potrebbero interagire tra loro i 2 scope delle funzioni? Posso accedere alla variabile outerVar
di outerFunc()
dall’ambito innerFunc()
?
Proviamolo nell’esempio:
function outerFunc() { // the outer scope let outerVar = 'I am outside!'; function innerFunc() { // the inner scope console.log(outerVar); // => logs "I am outside!" } innerFunc();}outerFunc();
Infatti, la variabile outerVar
è accessibile all’interno dello scope innerFunc()
. Le variabili dello scope esterno sono accessibili all’interno dello scope interno.
Ora sapete 2 cose interessanti:
- Gli scope possono essere annidati
- Le variabili dello scope esterno sono accessibili all’interno dello scope interno
3. Lo scope lessicale
Come fa JavaScript a capire che outerVar
dentro innerFunc()
corrisponde alla variabile outerVar
di outerFunc()
?
È perché JavaScript implementa un meccanismo di scoping chiamato scoping lessicale (o scoping statico). Lo scoping lessicale significa che l’accessibilità delle variabili è determinata dalla posizione delle variabili nel codice sorgente all’interno degli scopes annidati.
Semplicemente, lo scoping lessicale significa che all’interno dello scope interno si può accedere alle variabili dei suoi scope esterni.
Si chiama lexical (o static) perché il motore determina (al momento del lexing) l’annidamento degli scope semplicemente guardando il codice sorgente JavaScript, senza eseguirlo.
Ecco come il motore capisce lo snippet di codice precedente:
- Vedo che definisci una funzione
outerFunc()
che ha una variabileouterVar
. Bene. - All’interno del
outerFunc()
, vedo che definite una funzioneinnerFunc()
. - All’interno del
innerFunc()
, vedo una variabileouterVar
senza dichiarazione. Poiché utilizzo lo scoping lessicale, considero la variabileouterVar
all’interno diinnerFunc()
come la stessa variabile diouterVar
diouterFunc()
.
L’idea distillata dell’ambito lessicale:
L’ambito lessicale consiste in ambiti esterni determinati staticamente.
Per esempio:
const myGlobal = 0;function func() { const myVar = 1; console.log(myGlobal); // logs "0" function innerOfFunc() { const myInnerVar = 2; console.log(myVar, myGlobal); // logs "1 0" function innerOfInnerOfFunc() { console.log(myInnerVar, myVar, myGlobal); // logs "2 1 0" } innerOfInnerOfFunc(); } innerOfFunc();}func();
Lo scope lessicale di innerOfInnerOfFunc()
è costituito dagli scope di innerOfFunc()
func()
e dallo scope globale (lo scope più esterno). All’interno di innerOfInnerOfFunc()
è possibile accedere alle variabili dello scope lessicale myInnerVar
myVar
e myGlobal
.
Lo scopo lessicale di innerFunc()
consiste in func()
e nello scopo globale. All’interno di innerOfFunc()
è possibile accedere alle variabili dello scope lessicale myVar
e myGlobal
.
Infine, l’ambito lessicale di func()
consiste solo nell’ambito globale. All’interno di func()
è possibile accedere alla variabile dello scope lessicale myGlobal
.
4. La chiusura
Ok, lo scope lessicale permette di accedere staticamente alle variabili degli scope esterni. C’è solo un passo fino alla chiusura!
Guardiamo di nuovo l’esempio outerFunc()
e innerFunc()
:
function outerFunc() { let outerVar = 'I am outside!'; function innerFunc() { console.log(outerVar); // => logs "I am outside!" } innerFunc();}outerFunc();
Nell’ambito innerFunc()
, la variabile outerVar
è accessibile dall’ambito lessicale. Questo è già noto.
Nota che l’invocazione innerFunc()
avviene all’interno del suo scope lessicale (lo scope di outerFunc()
).
Facciamo una modifica: innerFunc()
essere invocato al di fuori del suo ambito lessicale (al di fuori di outerFunc()
innerFunc()
sarebbe ancora in grado di accedere a outerVar
?
Facciamo le modifiche allo snippet di codice:
function outerFunc() { let outerVar = 'I am outside!'; function innerFunc() { console.log(outerVar); // => logs "I am outside!" } return innerFunc;}const myInnerFunc = outerFunc();myInnerFunc();
Ora innerFunc()
viene eseguito fuori dal suo ambito lessicale. E ciò che è importante:
innerFunc()
ha ancora accesso a outerVar
dal suo ambito lessicale, anche essendo eseguito fuori dal suo ambito lessicale.
In altre parole, innerFunc()
chiude sopra (cioè cattura, ricorda) la variabile outerVar
dal suo ambito lessicale.
In altre parole, innerFunc()
è una chiusura perché chiude la variabile outerVar
dal suo ambito lessicale.
Hai fatto l’ultimo passo per capire cos’è una chiusura:
La chiusura è una funzione che accede al suo ambito lessicale anche eseguita fuori dal suo ambito lessicale.
Più semplicemente, la chiusura è una funzione che ricorda le variabili del luogo in cui è definita, indipendentemente da dove viene eseguita successivamente.
Una regola generale per identificare una chiusura: se vedete in una funzione una variabile aliena (non definita all’interno della funzione), molto probabilmente quella funzione è una chiusura perché la variabile aliena è catturata.
Nel precedente frammento di codice, outerVar
è una variabile aliena all’interno della chiusura innerFunc()
catturata dallo scope outerFunc()
.
Continuiamo con esempi che dimostrano perché la chiusura è utile.
5. Esempi di chiusura
5.1 Gestore di eventi
Visualizziamo quante volte viene cliccato un pulsante:
let countClicked = 0;myButton.addEventListener('click', function handleClick() { countClicked++; myText.innerText = `You clicked ${countClicked} times`;});
Aprire il demo e cliccare il pulsante. Il testo si aggiorna per mostrare il numero di clic.
Quando il pulsante viene cliccato, handleClick()
viene eseguito da qualche parte all’interno del codice DOM. L’esecuzione avviene lontano dal luogo di definizione.
Ma essendo una chiusura, handleClick()
cattura countClicked
dall’ambito lessicale e lo aggiorna quando avviene un clic. Ancora di più, anche myText
viene catturato.
5.2 Callbacks
Catturare le variabili dallo scope lessicale è utile nei callbacks.
Un setTimeout()
callback:
const message = 'Hello, World!';setTimeout(function callback() { console.log(message); // logs "Hello, World!"}, 1000);
Il callback()
è una chiusura perché cattura la variabile message
.
Una funzione iteratrice per forEach()
:
let countEven = 0;const items = ;items.forEach(function iterator(number) { if (number % 2 === 0) { countEven++; }});countEven; // => 2
Il iterator
è una chiusura perché cattura la variabile countEven
.
5.3 Programmazione funzionale
La chiusura avviene quando una funzione restituisce un’altra funzione fino a quando gli argomenti non sono completamente forniti.
Per esempio:
function multiply(a) { return function executeMultiply(b) { return a * b; }}const double = multiply(2);double(3); // => 6double(5); // => 10const triple = multiply(3);triple(4); // => 12
multiply
è una funzione curry che ritorna un’altra funzione.
Il curry, un concetto importante della programmazione funzionale, è possibile anche grazie alle chiusure.
executeMultiply(b)
è una chiusura che cattura a
dal suo ambito lessicale. Quando la chiusura è invocata, la variabile catturata a
e il parametro b
sono usati per calcolare a * b
.
6. Conclusione
Lo scope è ciò che regola l’accessibilità delle variabili in JavaScript. Ci può essere un ambito di funzione o di blocco.
Lo scope lessicale permette ad uno scope di funzione di accedere staticamente alle variabili degli scope esterni.
Infine, una chiusura è una funzione che cattura le variabili dal suo ambito lessicale. In parole semplici, la chiusura ricorda le variabili del luogo in cui è definita, non importa dove viene eseguita.
Le closure catturano le variabili all’interno di gestori di eventi, callback. Sono usate nella programmazione funzionale. Inoltre, potrebbe esservi chiesto come funzionano le chiusure durante un colloquio di lavoro Frontend.
Ogni sviluppatore JavaScript deve sapere come funzionano le chiusure. Affrontalo ⌐■■■■.
Che ne dici di una sfida? 7 domande di intervista sulle chiusure JavaScript. Puoi rispondere?