Hoisting
By Sergio Spina
- 7 minutes read - 1480 wordsOgni variabile o dichiarazione di funzione ha un proprio ambito di visibilità e utilizzabilità (scope). Ma all’interno di uno scope una data variabile o funzione è visibile e accessibile anche prima della sua formale dichiarazione. E’ il meccanismo dello hoisting.
Cosa è
To hoist in inglese significa sollevare qualcosa di pesante, a volte con appositi strumenti o corde; issare. In JavaScript è il meccanismo con il quale il compilatore (durante la fase del parsing) raccoglie tutte le dichiarazioni di funzione e di variabile trovate in uno scope e le sposta all’inizio dello scope stesso, in modo da poterle usare per tutti i riferimenti e le assegnazioni successive.
Questo significa che una certa variabile o funzione è accessibile e utilizzabile anche prima che sia stata dichiarata. Esempio:
1getFruit();
2
3function getFruit() {
4 console.log('Here you are a pineapple');
5}
Questo codice funziona come ci si aspetta, la chiamata a getFruit()
restituisce una stringa nella
console anche se la funzione chiamata è definita solo successivamente.
Come funziona
L’hoisting funziona in modo diverso a seconda che si tratti di dichiarazioni di funzione, di
variabili dichiarate con var
, o ancora di variabili dichiarate con let
o const
.
Dichiarazioni di funzione
Quando il compilatore JavaScript trova una dichiarazione di funzione crea all’inizio dello scope una dichiarazione di variabile con lo stesso nome della funzione e automaticamente inizializza la variabile con un puntatore alla stessa funzione. Poiché questa dichiarazione viene collocata all’inizio dello scope tutte le successive chiamate a quella funzione avranno un riferimento prestabilito.
Una particolarità da tenere presente è che nel caso delle funzioni lo scope all’inizio del quale
viene spostata la dichiarazione è quello della funzione immediatamente raggiungibile (o quello
globale), non lo scope del blocco for
o while
.
1function fruitList() {
2 let foods = ['banana'];
3
4 for (let food of foods) {
5 function getFruit() {
6 console.log(`Here you are a ${food}`);
7 }
8 }
9
10 getFruit();
11}
12
13fruitList(); // Here you are a banana
In questo esempio la chiamata a getFruit()
viene fatta all’interno della funzione ma, anche se la
dichiarazione avviene all’interno di un blocco for...of
, la chiamata ha successo in quanto il
compilatore ha ‘sollevato’ (hoisted) la dichiarazione di variabile getFruit
all’inizio dello
scope di fruitList()
e la ha immediatamente inizializzata come puntatore a funzione.
Dichiarazioni di variabile con var
Anche le dichiarazioni di variabili var
vengono spostate all’inizio dello scope (scope di
funzione e non di blocco, proprio come per le funzioni). Ma le variabili var
‘hoisted’ vengono
inizializzate a undefined
, a differenza che per le funzioni che sono immediatamente utilizzabili.
1console.log(smartAnimal); // undefined
2
3var smartAnimal = 'horse';
4
5console.log(smartAnimal); // horse
6console.log(dumbAnimal); // reference error: dumbAnimal is not defined
In questo esempio la dichiarazione della variabile smartAnimal
viene spostata all’inizio dello
scope. La chiamata da parte di console.log
infatti trova una variabile con quel nome ma ad essa
è stato assegnato il valore undefined
; subito dopo viene effettuata l’assegnazione di un valore
(‘horse’) alla variabile; la successiva chiamata da parte di console.log
dimostra che adesso la
variabile è inizializzata ad un valore.
Infine console.log
prova a usare una variabile mai creata, neanche successivamente. Qui non c’è
hoisting che tenga: non c’è nulla da spostare all’inizio dello scope e quindi il compilatore
solleva un errore.
Una cosa interessante (ma del tutto conseguente a quanto abbiamo visto finora) avviene se proviamo
ad utilizzare una funzione assegnata ad una variabile dichiarata con var
.
1getFruit(); // TypeError: getFruit is not a function
2getCity(); // NewYork
3
4var getFruit = function () {
5 console.log('Here you are an orange');
6};
7
8function getCity() {
9 console.log('New York');
10}
In questo caso la dichiarazione di getFruit
viene spostata all’inizio dello scope, e quindi è
utilizzabile; ma essendo una dichiarazione fatta con var
il compilatore la inizializza a
undefined
. Questo è il motivo per cui viene restituito un TypeError
: il compilatore trova una
variabile getFruit
ma non la riconosce come una funzione. Se invece avessimo dichiarato getFruit
direttamente come funzione il compilatore avrebbe immediatamente (come abbiamo visto nel capitolo
precedente) inizializzato la variabile ‘sollevata’ a funzione, e la chiamata avrebbe dato il
risultato corretto (come avviene per la chiamata a getCity
).
A questo punto il meccanismo di hoisting mostrato nel codice seguente è chiaro:
1console.log(getFruit); // undefined
2
3getCity(); // NewYork
4
5var getFruit = function () {
6 return 'Here you are an orange';
7};
8
9console.log(getFruit()); // Here you are an orange
10
11function getCity() {
12 console.log('New York');
13}
getFruit
viene dichiarata alla riga 5 con var
; viene quindi sollevata all’inizio dello scope e
inizializzata a undefined
, e la prima riga del codice lo conferma. Alla riga 5 alla variabile
getFruit
viene assegnata una funzione, e infatti adesso l’istruzione console.log
alla riga 9,
pur uguale a quella alla riga 1, restituisce la stringa corretta. La chiamata a getCity
alla riga
3, invece, restituisce subito una stringa nella console in quanto la variabile getCity
viene
dichiarata direttamente come funzione, seppure alla fine del programma.
Dichiarazioni di variabili con let e const
let
e const
sono acquisizioni piuttosto recenti all’armamentario degli strumenti per lo sviluppo
in JavaScript: e infatti, pur avendo la funzione di dichiarare variabili come var
e function
,
hanno un comportamento alquanto diverso dai loro predecessori:
- per inizializzare una variabile
var
è sufficiente una assegnazione; l’unico modo per inizializzare una variabilelet
oconst
è con una assegnazione collegata ad una istruzione di dichiarazione. - Le variabili
var
, dopo essere state ‘sollevate’ all’inizio dello scope corrente, vengono automaticamente inizializzate aundefined
; le variabililet
econst
non vengono inizializzate dopo l’hoisting e rimangono inaccessibili fino al momento in cui vengono formalmente dichiarate. - Le variabili
var
efunction
vengono ‘sollevate’ all’inizio del più vicino scope di funzione, ma non di blocco; le variabili dichiarate conlet
econst
vengono sollevate all’inizio dello scope corrente, anche se questo fosse uno scope di blocco. - Da ultimo, le variabili
var
possono essere ridichiarate all’interno dello stesso scope, con un effetto praticamente nullo; le variabililet
oconst
non possono essere ridichiarate, risultandone altrimenti la restituzione di un errore da parte del compilatore.
Approfondiamo i primi due punti:
1console.log(smartAnimal);
2// ReferenceError: Cannot access 'smartAnimal' before initialization
3
4let smartAnimal = 'dog';
In questo caso il compilatore JavaScript restituisce un errore in quanto la variabile ‘smartAnimal’ non è stata ancora inizializzata; si badi: il compilatore sa che la variabile esiste all’interno dello scope, ma non la considera accessibile in quanto non ancora inizializzata. Proviamo allora a inizializzarla prima di usarla:
1smartAnimal = 'dog';
2// ReferenceError: Cannot access 'smartAnimal' before initialization
3
4console.log(smartAnimal);
5
6let smartAnimal;
Ma anche qui viene restituito lo stesso errore, e proprio nel momento in cui si cerca di inizializzare la variabile con la stringa ‘dog’.
La TDZ
Il punto è che le variabili let
e const
vengono considerate ‘disponibili’ e ‘accessibili’ da
parte del compilatore solo nel momento in cui vengono formalmente dichiarate con le istruzioni let
o const
. In altre parole le variabili let
e const
vengono ‘sollevate’ all’inizio dello scope
in cui sono create, e perciò considerate esistenti, ma diventano anche accessibili solo nel
momento in cui vengono formalmente dichiarate. La finestra temporale in cui una variabile let
o
const
è ’esistente’ ma non ‘accessibile’ viene chiamata Temporal Death Zone (TDZ).
Vediamo il codice:
1var bigAnimal = 'elephant';
2
3{
4 console.log(bigAnimal);
5 // ReferenceError: Cannot access 'bigAnimal' before initialization
6
7 let bigAnimal = 'whale';
8}
In questo caso la variabile bigAnimal
viene inizializzata all’inizio del programma con la stringa
’elephant’; ciononostante viene sollevato un errore nel momento in cui si cerca di accedervi.
Il punto è che all’interno del blocco c’è un’altra variabile bigAnimal
che viene dichiarata con
let
, che pertanto viene ‘sollevata’ all’inizio del blocco corrente ma non inizializzata; questa
nuova variabile quindi ’eclissa’ la prima agli occhi del compilatore il quale giustamente ci
notifica di non poter accedere a bigAnimal
in quanto non ancora inizializzata.
Si tratta di un errore dovuto proprio alla presenza di una TDZ tra il momento in cui una variabile è semplicemente esistente e il momento in cui è invece effettivamente accessibile.
Conclusioni
L’hoisting è un comportamento complesso del compilatore JavaScript, reso ancora più criptico dal fatto di gestire in modo molto diverso le variabili e le funzioni a seconda di come e quando vengono dichiarate. La Temporal Death Zone, conseguenza diretta del meccanismo di hoisting, pure incide in modo molto diverso sulla gestione dei vari tipi di variabile. Tutta questa complessità e disomogeneità di comportamento può certamente essere fonte di errori insidiosi e difficilmente individuabili.
L’unico modo per cercare di evitare questi errori sembra essere quello di dichiarare tutte le variabili e le funzioni all’inizio dello scope di riferimento, riducendo al minimo il lavoro di hoisting da parte del compilatore e azzerando la durata della TDZ.
Nel banner: Donald Knuth, autore di “The art of computer programming”, creatore del software TeX e del suo manuale “The TeXbook”.