Closures – molti di voi sviluppatori JavaScript hanno probabilmente già sentito questo termine. Quando ho iniziato il mio viaggio con JavaScript, ho incontrato spesso le chiusure. E penso che siano uno dei concetti più importanti e interessanti in JavaScript.
Non pensi che siano interessanti? Questo succede spesso quando non si capisce un concetto – non lo si trova interessante. (Non so se questo succede a voi o no, ma questo è il caso di me).
Così in questo articolo, cercherò di rendere le chiusure interessanti per voi.
Prima di entrare nel mondo delle chiusure, cerchiamo di capire lo scoping lessicale. Se lo conoscete già, saltate la prossima parte. Altrimenti saltatela per capire meglio le chiusure.
Lexical Scoping
Potreste pensare – conosco lo scope locale e globale, ma che diavolo è lo scope lessicale? Ho reagito allo stesso modo quando ho sentito questo termine. Non preoccupatevi! Diamo un’occhiata più da vicino.
È semplice come gli altri due scope:
function greetCustomer() { var customerName = "anchal"; function greetingMsg() { console.log("Hi! " + customerName); // Hi! anchal } greetingMsg();}
Si può vedere dall’output precedente che la funzione interna può accedere alla variabile della funzione esterna. Questo è scoping lessicale, dove lo scopo e il valore di una variabile è determinato da dove è definita/creata (cioè la sua posizione nel codice). Capito?
So che l’ultima parte potrebbe avervi confuso. Quindi lasciate che vi porti più a fondo. Sapevate che lo scoping lessicale è anche conosciuto come scoping statico? Sì, questo è il suo altro nome.
C’è anche lo scoping dinamico, che alcuni linguaggi di programmazione supportano. Perché ho menzionato lo scoping dinamico? Perché può aiutarvi a capire meglio lo scoping lessicale.
Guardiamo alcuni esempi:
function greetingMsg() { console.log(customerName);// ReferenceError: customerName is not defined}function greetCustomer() { var customerName = "anchal"; greetingMsg();}greetCustomer();
Sei d’accordo con l’output? Sì, darà un errore di riferimento. Questo perché entrambe le funzioni non hanno accesso all’ambito dell’altra, poiché sono definite separatamente.
Guardiamo un altro esempio:
function addNumbers(number1) { console.log(number1 + number2);}function addNumbersGenerate() { var number2 = 10; addNumbers(number2);}addNumbersGenerate();
L’output di cui sopra sarà 20 per un linguaggio con scoping dinamico. Le lingue che supportano lo scoping lessicale daranno referenceError: number2 is not defined
. Perché?
Perché nello scoping dinamico, la ricerca avviene prima nella funzione locale, poi va nella funzione che ha chiamato quella funzione locale. Poi cerca nella funzione che ha chiamato quella funzione, e così via, su per lo stack delle chiamate.
Il suo nome è auto esplicativo – “dinamico” significa cambiamento. Lo scopo e il valore della variabile possono essere diversi, poiché dipendono da dove viene chiamata la funzione. Il significato di una variabile può cambiare a runtime.
Hai capito il concetto di scoping dinamico? Se sì, allora ricordate che lo scoping lessicale è il suo opposto.
Nello scoping lessicale, la ricerca avviene prima nella funzione locale, poi va nella funzione all’interno della quale quella funzione è definita. Poi cerca nella funzione all’interno della quale quella funzione è definita e così via.
Quindi, lo scoping lessicale o statico significa che l’ambito e il valore di una variabile sono determinati da dove è definita. Non cambia.
Guardiamo di nuovo l’esempio precedente e cerchiamo di capire il risultato da soli. Solo un colpo di scena – dichiarate number2
all’inizio:
var number2 = 2;function addNumbers(number1) { console.log(number1 + number2);}function addNumbersGenerate() { var number2 = 10; addNumbers(number2);}addNumbersGenerate();
Sapete quale sarà l’output?
Corretto – è 12 per le lingue con scoping lessicale. Questo perché prima cerca in una funzione addNumbers
(ambito più interno) poi cerca all’interno, dove questa funzione è definita. Poiché ottiene la variabile number2
, significa che l’output è 12.
Vi starete chiedendo perché ho speso così tanto tempo sullo scoping lessicale qui. Questo è un articolo di chiusura, non di scoping lessicale. Ma se non conoscete lo scoping lessicale allora non capirete le chiusure.
Perché? Avrete la vostra risposta quando vedremo la definizione di una chiusura. Quindi entriamo in pista e torniamo alle chiusure.
Che cos’è una chiusura?
Guardiamo la definizione di una chiusura:
La chiusura si crea quando una funzione interna ha accesso alle variabili e agli argomenti della funzione esterna. La funzione interna ha accesso a –
1. Le proprie variabili.
2. Variabili e argomenti della funzione esterna.
3. Variabili globali.
Aspettate! Questa è la definizione di una chiusura o di scoping lessicale? Entrambe le definizioni sembrano uguali. Come sono diverse?
Ecco perché ho definito lo scoping lessicale sopra. Perché le chiusure sono legate allo scoping lessicale/statico.
Guardiamo di nuovo l’altra definizione che vi dirà come le chiusure sono diverse.
La chiusura è quando una funzione è in grado di accedere al suo ambito lessicale, anche quando quella funzione sta eseguendo al di fuori del suo ambito lessicale.
Oppure,
Le funzioni interne possono accedere al loro ambito genitore, anche dopo che la funzione genitore è già stata eseguita.
Confuso? Non preoccupatevi se non avete ancora capito il punto. Ho degli esempi per aiutarvi a capire meglio. Modifichiamo il primo esempio di scoping lessicale:
function greetCustomer() { const customerName = "anchal"; function greetingMsg() { console.log("Hi! " + customerName); } return greetingMsg;}const callGreetCustomer = greetCustomer();callGreetCustomer(); // output – Hi! anchal
La differenza in questo codice è che noi restituiamo la funzione interna e la eseguiamo in seguito. In alcuni linguaggi di programmazione, la variabile locale esiste durante l’esecuzione della funzione. Ma una volta che la funzione viene eseguita, quelle variabili locali non esistono e non saranno accessibili.
Qui, tuttavia, la scena è diversa. Dopo che la funzione madre è stata eseguita, la funzione interna (funzione restituita) può ancora accedere alle variabili della funzione madre. Sì, avete indovinato. Le chiusure sono la ragione.
La funzione interna conserva il suo ambito lessicale quando la funzione genitore è in esecuzione e quindi, in seguito la funzione interna può accedere a quelle variabili.
Per capire meglio, usiamo il metodo dir()
della console per guardare nella lista delle proprietà di callGreetCustomer
:
console.dir(callGreetCustomer);
Dall’immagine precedente, potete vedere come la funzione interna conserva il suo ambito genitore (customerName
) quando greetCustomer()
viene eseguito. E più tardi, usa customerName
quando callGreetCustomer()
viene eseguito.
Spero che questo esempio vi abbia aiutato a capire meglio la definizione di chiusura di cui sopra. E forse ora troverete le chiusure un po’ più divertenti.
E adesso? Rendiamo questo argomento più interessante guardando diversi esempi.
Esempi di chiusure in azione
function counter() { let count = 0; return function() { return count++; };}const countValue = counter();countValue(); // 0countValue(); // 1countValue(); // 2
Ogni volta che chiamate countValue
, il valore della variabile count viene incrementato di 1. Aspetta – pensavi che il valore di count fosse 0?
Beh, sarebbe sbagliato perché una chiusura non lavora con un valore. Memorizza il riferimento della variabile. Ecco perché, quando aggiorniamo il valore, si riflette nella seconda o terza chiamata e così via, poiché la chiusura memorizza il riferimento.
Sentite un po’ più chiaro ora? Guardiamo un altro esempio:
function counter() { let count = 0; return function () { return count++; };}const countValue1 = counter();const countValue2 = counter();countValue1(); // 0countValue1(); // 1countValue2(); // 0countValue2(); // 1
Spero che abbiate indovinato la risposta giusta. Se no, ecco la ragione. Come countValue1
e countValue2
, entrambi conservano il proprio ambito lessicale. Hanno ambienti lessicali indipendenti. Potete usare dir()
per controllare il valore ]
in entrambi i casi.
Guardiamo un terzo esempio.
Questo è un po’ diverso. In esso, dobbiamo scrivere una funzione per ottenere l’output:
const addNumberCall = addNumber(7);addNumberCall(8) // 15addNumberCall(6) // 13
Semplice. Usa la tua nuova conoscenza delle chiusure:
function addNumber(number1) { return function (number2) { return number1 + number2; };}
Ora guardiamo qualche esempio difficile:
function countTheNumber() { var arrToStore = ; for (var x = 0; x < 9; x++) { arrToStore = function () { return x; }; } return arrToStore;}const callInnerFunctions = countTheNumber();callInnerFunctions() // 9callInnerFunctions() // 9
Ogni elemento dell’array che memorizza una funzione ti darà un output di 9. Hai indovinato? Spero di sì, ma lasciate comunque che vi dica la ragione. Questo è dovuto al comportamento della chiusura.
La chiusura memorizza il riferimento, non il valore. La prima volta che il ciclo viene eseguito, il valore di x è 0. Poi la seconda volta x è 1, e così via. Poiché la chiusura memorizza il riferimento, ogni volta che il ciclo viene eseguito cambia il valore di x. E alla fine, il valore di x sarà 9. Quindi callInnerFunctions()
dà un output di 9.
Ma se volete un output da 0 a 8? Semplice! Usate una chiusura.
Pensateci prima di guardare la soluzione qui sotto:
function callTheNumber() { function getAllNumbers(number) { return function() { return number; }; } var arrToStore = ; for (var x = 0; x < 9; x++) { arrToStore = getAllNumbers(x); } return arrToStore;}const callInnerFunctions = callTheNumber();console.log(callInnerFunctions()); // 0console.log(callInnerFunctions()); // 1
Qui, abbiamo creato scope separati per ogni iterazione. Potete usare console.dir(arrToStore)
per controllare il valore di x in ]
per diversi elementi dell’array.
Ecco fatto! Spero che ora possiate dire che trovate le chiusure interessanti.
Per leggere altri miei articoli, guardate il mio profilo qui.