Il Lexical Scope
By Sergio Spina
- 5 minutes read - 1043 wordsJavaScript usa un particolare modello per gestire l’accesso alle variabili, chiamato lexical
scope. È proprio il lexical scope che consente al motore JavaScript di distinguere tra variabili
diverse ma con lo stesso nome, riconoscere variabili all’interno di funzioni o di cicli for
,
accedere a variabili globali dall’interno di una funzione o di un blocco.
Vediamo come funziona.
Il compilatore JavaScript
JavaScript è un linguaggio compilato. Prima della esecuzione il codice passa per una fase di vera e propria compilazione, suddivisa in tre sotto-fasi:
-
tokenizing/lexing: il codice viene suddiviso in cellule indivisibili chiamate tokens, ognuna delle quali rappresenta un elemento significativo. Ad esempio, la linea
1let animal = 'fox';
viene suddivisa nei 5 tokens
let
,animal
,=
,'fox'
,;
(il punto e virgola è un token). -
parsing: la lista di tokens viene organizzata in una struttura ad albero chiamata abstract syntax tree (AST).
-
generazione del codice: l’AST viene convertito in codice eseguibile.
È vero che il compilatore JavaScript non produce un file eseguibile e distribuibile indipendentemente dal codice sorgente, come normalmente avviene con i linguaggi compilati, ma questo non cambia la natura di JavaScript di essere un linguaggio compilato a tutti gli effetti.
Orbene, lo scope viene creato proprio durante la fase di compilazione, da cui deriva il nome di lexical scope (da tokenizing/lexing), e resta immutabile durante l’esecuzione del programma.
La creazione dello scope
Lo scope può essere definito come l’ambito all’interno del quale vigono certe regole di accesso alle variabili. Abbiamo visto che viene creato durante la compilazione del codice e non viene più modificato durante l’esecuzione del programma.
La struttura dello scope è determinata dal modo con il quale vengono dichiarate le variabili, le
funzioni e i blocchi (for
, while
, ecc). Il procedimento è il seguente:
quando il compilatore inizia l’analisi del codice crea un scope “globale”: durante l’analisi del codice il compilatore può incontrare variabili, funzioni, o blocchi:
- se incontra una nuova dichiarazione di variabile il compilatore la assegna allo scope corrente, che in questo caso è quello globale;
- se invece incontra una dichiarazione di funzione o un blocco apre un nuovo scope all’interno di quello corrente. Il nuovo scope diventa quello corrente e tutte le nuove dichiarazioni di variabile vengono assegnate al nuovo scope;
- questo avviene fino alla chiusura dello scope della funzione o del blocco (che solitamente
avviene con la graffa di chiusura “
}
”). Lo scope corrente non è più quello appena chiuso, ma quello precedente, ovvero quello all’interno del quale era stata aperta la funzione o il blocco.
Variabili diverse con nomi uguali
Le variabili dichiarate in scope diversi sono considerate sempre variabili diverse, anche se hanno lo stesso nome. Quando il compilatore incontra una assegnazione di un valore ad una variabile deve quindi determinare quale variabile, tra le diverse con lo stesso nome dichiarate all’interno del programma, riceverà l’assegnazione del valore.
Inizia quindi a cercare nello scope corrente la dichiarazione di una variabile con quel nome; se la trova ferma la ricerca ed esegue l’assegnazione, altrimenti continua la ricerca nello scope esterno, quello all’interno del quale lo scope corrente si trova ed è stato creato.
Questa ricerca (da uno scope a quello esterno, a quello ancora più esterno) prosegue fino a quando il compilatore arriva a cercare nello scope globale, quello più esterno di tutti. Se non trova la dichiarazione di variabile neanche lì allora solleva un errore.
Si noti che il compilatore effettua la ricerca sempre in linea retta e sempre verso scope esterni a quello corrente.
Qualche esempio
1// inizio dello scope globale
2let animal = 'fox';
3
4function getAnimal() {
5 // scope della funzione
6 console.log('from inside the function:', animal);
7}
8// chiusa la funzione si chiude anche il suo scope;
9// siamo tornati nello scope globale
10
11getAnimal();
12console.log('from outside the function:', animal);
13
14// from inside the function: fox
15// from outside the function: fox
All’inizio del programma il compilatore trova la variabile animal
, che viene assegnata allo scope
globale. Subito dopo trova una dichiarazione di funzione alla quale assegna un nuovo scope, annidato
in quello globale; trova quindi una richiesta di valore ad una variabile effettuata dalla istruzione
console.log
: il compilatore cerca la dichiarazione di variabile nello scope corrente (quello
della funzione getAnimal
) e non la trova. Inizia quindi a cercare nello scope immediatamente
esterno a quello della funzione e trova la formale dichiarazione let animal = 'fox'
. All’interno
della funzione la variabile animal
viene riconosciuta come quella dichiarata all’inizio del
programma e l’output delle ultime due righe lo conferma.
1let animal = 'fox';
2
3function getAnimal() {
4 let animal = 'horse';
5 console.log('from inside the function:', animal);
6}
7
8getAnimal();
9console.log('from outside the function:', animal);
10
11// from inside the function: horse
12// from outside the function: fox
Questo programma è quasi identico a quello precedente, l’unica differenza è che, quando il
compilatore inizia a cercare una dichiarazione di variabile cui assegnare il riferimento a animal
all’interno di console.log
, lo trova subito nello scope corrente, quindi interrompe la ricerca.
L’output alla fine del programma rende conto di questa differenza.
Una trappola
Dunque se il compilatore trova una assegnazione a variabile, ma non trova una dichiarazione formale di quella variabile nè nello scope corrente nè in nessuno di quelli superiori, solleva un errore.
O meglio: dovrebbe sollevare un errore. Purtroppo non è sempre così.
1function getAnimalName() {
2 // viene assegnato un valore ad una variabile mai dichiarata:
3 animal = 'fox';
4}
5
6getAnimalName();
7
8// output del valore di una variabile che /non dovrebbe/ esistere
9console.log(animal);
10// 'fox'
11// da dove viene fuori questo valore?
È un problema di cui JavaScript ha sempre sofferto: se si assegna un valore ad una variabile non dichiarata, JavaScript crea silenziosamente una variabile con quel nome nello scope globale, con il concreto rischio di generare bug insidiosi.
Si può però costringere il compilatore a sollevare un errore in presenza di qualunque variabile non formalmente dichiarata facendo girare il programma (o la funzione) in strict mode.
1'use strict';
2
3function getAnimalName() {
4 // viene assegnato un valore ad una variabile mai dichiarata:
5 animal = 'fox';
6 // ReferenceError: la compilazione si ferma qui,
7 // il programma non sarà eseguito.
8}
9
10getAnimalName();
11
12console.log(animal);
Nel banner: Richard Stallman, creatore della Free Software Foundation e dell'editor di testo Emacs.