Articolo originale: https://www.freecodecamp.org/news/what-is-hoisting-in-javascript/

In JavaScript, l'hoisting consente di utilizzare funzioni e variabili prima che siano dichiarate. In questo articolo impareremo cos'è l'hoisting e come funziona.

Cos'è l'hoisting?

Dai un'occhiata al codice qui sotto e indovina cosa accade quando viene eseguito:

console.log(foo);
var foo = 'foo';

Potrebbe sorprenderti che l'output di questo codice è undefined e che non genera un errore – anche se foo viene assegnata dopo il console.log!

Questo perché l'interprete JavaScript divide dichiarazione e assegnazione di funzioni e variabili: "innalza" le dichiarazioni in cima all'ambito in cui sono contenute prima dell'esecuzione.

Questo processo è chiamato hoisting (dall'inglese hoist: sollevare, issare) e, nell'esempio precedente, consente di utilizzare foo prima che sia dichiarata.

Diamo un'occhiata più approfondita all'hoisting di variabili e funzioni per capire cosa vuol dire e come funziona.

Hoisting di variabili in JavaScript

Tanto per rinfrescarci la memoria: dichiariamo una variabile con le parole chiave var, let e const. Ad esempio:

var foo;
let bar;

E assegniamo un valore a una variabile con l'operatore di assegnazione:

// Dichiarazione
var foo;
let bar;

// Assegnazione
foo = 'foo';
bar = 'bar';

In molti casi, possiamo combinare dichiarazione e assegnazione in un solo passaggio:

var foo = 'foo';
let bar = 'bar';
const baz = 'baz';

L'hoisting delle variabili funziona differentemente a seconda di come la variabile viene dichiarata. Partiamo con le variabili dichiarate con var.

Hoisting di variabili con var

Quando l'interprete effettua l'hoisting di una variabile dichiarata con var, ne inizializza il valore come undefined. L'output della prima riga di codice qui sotto è undefined:

console.log(foo); // undefined

var foo = 'bar';

console.log(foo); // "bar"

Come abbiamo già detto, l'hoisting deriva dall'interprete che divide la dichiarazione e l'assegnazione di una variabile. Possiamo ottenere lo stesso comportamento dividendo manualmente la dichiarazione e l'assegnazione in due passaggi:

var foo;

console.log(foo); // undefined

foo = 'foo';

console.log(foo); // "foo"

Ricorda che l'output del primo console.log(foo) è undefined perché foo è soggetto a hoisting e gli viene dato un valore predefinito (non perché la variabile non viene mai dichiarata). Usare una variabile non dichiarata genererebbe un errore ReferenceError:

console.log(foo); // Uncaught ReferenceError: foo is not defined

Anche usare una variabile non dichiarata prima prima della sua assegnazione causerebbe un errore ReferenceError poiché non è avvenuto l'hoisting di alcuna dichiarazione:

console.log(foo); // Uncaught ReferenceError: foo is not defined
foo = 'foo';      // Assegnare una variabile non dichiarata è valido

Ora potresti pensare "È piuttosto strano che JavaScript ci faccia accedere a variabili prima che siano dichiarate". Questo comportamento è una parte inusuale di JavaScript e può portare a errori. Usare una variabile prima della sua dichiarazione è solitamente non desiderabile.

Fortunatamente le variabili dichiarate con let e const, introdotte con ECMAScript 2015, si comportano in modo diverso.

Hoisting di variabili con let e const

Le variabili dichiarate con let e const subiscono l'hoisting ma non vengono inizializzate con un valore predefinito. Accedere a una variabile dichiarata con let o const prima della dichiarazione comporta un ReferenceError:

console.log(foo); // Uncaught ReferenceError: Cannot access 'foo' before initialization

let foo = 'bar';  // Lo stesso vale per le variabili dichiarate con const

L'interprete effettua l'hoisting di foo: il messaggio di errore ci dice che la variabile è inizializzata da qualche parte.

Temporal Dead Zone

La ragione per cui otteniamo un errore quando proviamo ad accedere a una variabile dichiarata con let o const prima della sua dichiarazione è la temporal dead zone (TDZ, letteralmente zona temporale morta).

La TDZ parte all'inizio dell'ambito che racchiude la variabile e termina quando viene dichiarata. Accedere alla variabile durante la TDZ causa un ReferenceError.

Ecco un esempio con un blocco esplicito che mostra l'inizio e la fine del TDZ di foo:

{
 	// Inizio della TDZ di foo
  	let bar = 'bar';
	console.log(bar); // "bar"

	console.log(foo); // ReferenceError perché siamo nella TDZ

	let foo = 'foo';  // Fine della TDZ di foo
}

La TDZ è presente anche nei parametri di default delle funzioni, che vengono valutati da sinistra a destra. Nel seguente esempio, bar è nella TDZ finché non viene definito il suo valore di default:

function foobar(foo = bar, bar = 'bar') {
  console.log(foo);
}
foobar(); // Uncaught ReferenceError: Cannot access 'bar' before initialization

Ma il seguente codice funziona perché stiamo accedendo a foo fuori dalla sua TDZ:

function foobar(foo = 'foo', bar = foo) {
  console.log(bar);
}
foobar(); // "foo"

typeof nella temporal dead zone

Usare delle variabili dichiarate con let o const come operandi di un operatore typeof nella TDZ causerà un errore:

console.log(typeof foo); // Uncaught ReferenceError: Cannot access 'foo' before initialization
let foo = 'foo';

Questo comportamento è coerente con gli altri casi con let e const nel TDZ che abbiamo visto. La ragione per cui otteniamo ReferenceError è che foo è dichiarata ma non inizializzata – dovremmo essere consapevoli che la stiamo usando prima dell'inizializzazione (fonte: Axel Rauschmayer).

Tuttavia, ciò non avviene con var prima della dichiarazione perché l'inizializzazione avviene con undefined durante l'hoisting:

console.log(typeof foo); // "undefined"
var foo = 'foo';

Oltretutto, ciò è sorprendente perché possiamo verificare il tipo di una variabile che non esiste senza ottenere un errore. typeof restituisce una stringa:

console.log(typeof foo); // "undefined"

Infatti, l'introduzione di let e const ha interrotto la garanzia che typeof restituisca come valore sempre una stringa per ogni operando.

Hoisting di funzioni in JavaScript

Anche per le dichiarazioni di funzioni avviene l'hoisting. Ciò consente di chiamare una funzione prima che sia definita. Ad esempio, il seguente codice viene eseguito con successo e dà "foo" come output:

foo(); // "foo"

function foo() {
	console.log('foo');
}

Nota che l'hoisting avviene solo per le dichiarazioni di funzioni e non per le funzioni espressioni (function expression). Perfettamente sensato se pensiamo che abbiamo appena imparato che l'hoisting non avviene per l'assegnazione di variabili.

Se proviamo a chiamare una variabile assegnata a una funzione espressione, otterremo TypeError o ReferenceError, a seconda dell'ambito della variabile:

foo(); // Uncaught TypeError: foo is not a function
var foo = function () { }

bar(); // Uncaught ReferenceError: Cannot access 'bar' before initialization
let bar = function () { }

baz(); // Uncaught ReferenceError: Cannot access 'baz' before initialization
const baz = function () { }

Questo è diverso dal chiamare una funzione che è mai dichiarata, che genera un ReferenceError diverso:

foo(); // Uncaught ReferenceError: baz is not defined

Come usare l'hoisting in JavaScript

Hoisting di variabili

Per via della confusione che può creare l'hoisting di variabili dichiarate con var, è meglio evitare di usare delle variabili prima che siano dichiarate. Se stai scrivendo il codice di un progetto greenfield, dovresti usare let e const.

Se stai lavorando su un vecchio codebase o devi usare var per un'altra ragione, MDN raccomanda di scrivere le dichiarazioni con var più vicino possibile alla cima del loro ambito. In questo modo la visibilità delle variabili sarà più chiara.

Puoi anche considerare di usare la regola di ESLint no-use-before-define (non usare prima di definire), che ti assicura di non usare una variabile prima della sua dichiarazione.

Hoisting di funzioni

L'hoisting di funzioni è utile perché possiamo nascondere l'implementazione nella parte inferiore di un file e lasciare che chi legge si possa concentrare su quello che sta facendo il codice. In altre parole, possiamo aprire un file e vedere cosa fa il codice senza prima capire come è implementato.

Prendi questo esempio:

resetPunteggio();
disegnaTabellone();
popolaTabellone();
avviaGioco();

function resetPunteggio() {
	console.log("Sto resettando il punteggio");
}

function disegnaTabellone() {
	console.log("Sto disegnando il tabellone");
}

function popolaTabellone() {
	console.log("Sto popolando il tabellone");
}

function avviaGioco() {
	console.log("Sto avviando il gioco");
}

Abbiamo immediatamente un'idea di cosa fa il codice senza dover leggere tutte le dichiarazioni di funzione.

Comunque, usare le funzioni prima della dichiarazione è una questione di preferenza personale. Alcuni sviluppatori, come Wes Bos, preferiscono evitarlo e mettono le funzioni in moduli che possono essere importati al bisogno (fonte: Wes Bos).

La guida di stile di Airbnb va un passo oltre e incoraggia l'uso di function expression con nome rispetto alle dichiarazioni per evitare i riferimenti prima della dichiarazione:

Le dichiarazioni di funzione sono soggette a hoisting, il che vuol dire che è facile – troppo facile – fare riferimento a una funzione prima che sia definita in un file. Ciò inficia la leggibilità e la manutenibilità.

Se trovi che una definizione di funzione è abbastanza lunga o complessa, tanto da interferire con la comprensione del resto del file, allora potrebbe essere il momento di inserirla in un suo modulo! (fonte: Airbnb JavaScript Style Guide)

Conclusione

Grazie per aver letto, spero che questo articolo ti abbia aiutato a imparare qualcosa sull'hoisting in JavaScript. Contattami pure su LinkedIn se vuoi connetterti o se hai delle domande!