Le specifiche ECMA 6 hanno introdotto una funzionalità a mio avviso fantastica, in un linguaggio di programmazione già a suo modo fantastico: gli identificatori di variabili let
e const
.
Sebbene "const" sia un concetto più immediato da capire, le differenze tra "var" e "let" non sono sempre immediate. Lo scopo di questo articolo è appunto quello di fare maggiore luce su queste importanti differenze.
Lo scope
Prima di addentrarci nell’analisi dei concetti di "var" e "let" e verificare quali differenze sussistono tra loro, è bene chiarificare cosa si intende per "scope". In Javascript lo scope non è nient’altro che un blocco (o una zona) specifico di codice, che si differenzia dagli altri blocchi/zone. Lo scope globale è la zona in cui viene eseguito il codice Javascript nella sua interezza, ma questa zona a sua volta può avere (e nella maggior parte dei casi ha) svariate sotto-zone/sotto-blocchi di codice. Ad esempio, una variabile globale dichiarata in un punto qualsiasi dello script, dal suo inizio alla sua fine, è definita nello scope globale:
var myVar = "hello";
Se invece una variabile è definita in un ciclo, o in un blocco di codice esplicito (che si produce avviluppando le righe di codice in parentesi graffe "{" e "}"), lo scope viene definito scope di blocco, ad esempio:
for(var myVar = 0; myVar < 10; myVar++) {
// scope del blocco for
console.log(myVar;)
}
while(x < y) {
// scope del blocco while
}
{
var myVar = "hello";
// scope del blocco esplicito
}
Lo scope locale è invece quella particoalre zona di codice che è definita all’interno delle funzioni e dei metodi degli oggetti:
function myFunctionName() {
// scope locale della funzione myFunctionName
var x, y, z;
}
var myObjectName = {
'myMethodName': function() {
// scope locale del metodo myObjectName
var x, y, z;
}
}
Utilizzando le specifiche Javascript, sappiamo che una variabile definita con "var" nello scope globale è disponibile anche nello scope locale delle funzioni, dei metodi e dei blocchi di codice, contrariamente una variabile locale definita in una funzione o in un metodo NON è disponibile nello scope globale. Nota: le variabili definite con la keyword "var" negli scope di blocco sono disponbili anche nello scope globale e le variabili definite con la keyword "var" nello scope globale sono disponibili anche negli scope di blocco: utilizzando questo identificatore le variabili non risentono dello scope:
var myVar = "hello"
for (var counter = 0; counter < 10; counter++) {
// stampa "hello": lo scope globale e lo scope di blocco si sovrappongono con "var"
console.log(myVar);
}
// stampa 10: "counter" è disponibile anche nello scope globale
console.log(counter);
Scope globale vs Scope di blocco
Per capire la differenza fondamentale tra i due identificatori di variabili, possiamo dire molto sinteticamente che "var" viene utilizzato quando non abbiamo necessità di uno scope locale o uno scope di blocco, il che significa che la variabile può essere ridichiarata ed utilizzata senza problemi all’interno dello script, mentre "let" viene utilizzato quando abbiamo necessità di operazioni in uno scope locale o di blocco. In questo caso, variabili "let" con lo stesso namespace di altre variabili let dichiarate in un blocco differente, sono perfettamente legali, a differenza di quelle dichiarate con "var" che verranno sovrascritte. Vediamo un semplice esempio:
let x = 1;
if (x === 1) {
let x = 2;
// output: 2
console.log(x);
}
// output: 1
console.log(x);
In questo codice la variabile "di blocco" chiamata "x" NON può essere utilizzata globalmente e dunque ridichiarata accidentalmente. Questa distinzione permette un più ampio respiro allo sviluppatore Javascript, che possiede ora un controllo totale sulla natura, sulla tipologia e sul comportamento delle variabili che va a definire nella sua applicazione.
Un comportamento ingannevole per lo sviluppatore novizio può essere rappresentato in questo senso dai loop, come il loop for
. Ricorda che il loop definisce uno scope di blocco, dunque una variabile "var" manterrà l’ultimo valore impostato dal ciclo:
var myvalue = 0;
for (var myvalue = 10; myvalue < 100; myvalue+=10) {
// output 10 20 30 40 50 60 70 80 90 100
console.log( myvalue );
}
// output 100
console.log( myvalue );
mentre la variabile "let" sarà inalterata, a causa della sua natura dipendente dagli scope di blocco:
let myvalue = 0;
for (let myvalue = 10; myvalue < 100; myvalue+=10) {
// output 10 20 30 40 50 60 70 80 90 100
console.log( myvalue );
}
// output 0
console.log( myvalue );
Oggetto window
Tuttavia, sia "var" sia "let" possono essere tranquillamente utilizzati per definire variabili nello scope globale, dunque in questo caso specifico, apparentemente non ci sono differenze, ad esempio:
var myGlobalVar = 10;
let myGlobalLet = 20;
Ma questa affermazione non è corretta. Esiste una differenza fondamentale tra una variabile dichiarata con "var" nello scope globale ed una variabile dichiarata con "let" nello scope globale: la prima sarà disponibile come proprietà dell’oggetto globale window
, mentre la seconda non sarà disponibile in questo senso!
Dunque, nello snippet precedente potremo utilizzare la seguente riga:
var myGlobalVar = 10;
console.log( window.myGlobalVar ); // logga 10
ma non potremo scrivere:
let myGlobalLet = 20;
console.log( window.myGlobalLet ); // logga undefined
perchè la proprietà window.myGlobalLet
non è definita.
Questa differenza è molto importante per applicazioni Javascript che utilizzano stringhe di identificatori per utilizzare il valore delle variabili globali per mezzo dell’oggetto window, come nel seguente codice:
var name_1 = "riccardo";
var name_2 = "valentina";
var p = document.getElementById('p');
for(var i = 1; i<3; i++)
p.innerHTML += " " + window["name_" + i];
In questo esempio, molto semplice, viene utilizzato un elemento HTML come contenitore del valore di due variabili, definite con "name_1" e "name_2". Dato che sono variabili globali dichiarate con l’identificatore "var", saranno disponibili come proprietà globali dell’oggetto window
. Per questo motivo, dato che le variabili hanno namespace identico, è possibile utilizzarle tramite il suddetto oggetto window
attraverso la nozione dichiarativa (che lo utilizza come se fosse un array associativo, utilizzando come identificatore una stringa tra parentesi quadre). Nel nostro caso iteriamo tra le variabili utilizzando un semplice loop for
, e l’elemento container sarà riempito con la stringa "riccardo valentina". Questa strategia è usata per produrre API anche molto complesse, ma è bene ricordare che nello scope globale, NON sarà possibile utilizzarla con variabili dichiarate con l’identificatore let.
Ridichiarazione
Un’altra delle differenze di cui è necessario essere consapevoli riguarda anche l’operazione di ridichiarazione delle variabili, che con l’operatore "var" è assolutamente legale, mentre con l’operatore "let" non lo è, dato che il codice andrà a produrre un errore di tipo SyntaxError
:
if (x == myValue) {
let foo = 10;
let foo; // errore SyntaxError
}
Non è dunque possibile ridichiarare una variabile "let" all’interno dello stesso scope.
Sebbene in questo snippet di codice il comportamento di "let" (che nel contesto della dichiarazione si pone a metà tra l’operatore "var" e l’operatore "const") sia chiaramente prevedibile, esistono delle situazioni in cui esso diviene molto più subdolo, come nel caso di un operatore switch
. Vediamo un esempio:
let myValue = 1;
switch(myValue) {
case 0:
let foo;
break;
case 1:
let foo; // errore SyntaxError
break;
}
Come possiamo vedere, il codice produce un errore di tipo SyntaxError
perchè il blocco switch
cosi composto è considerato come un unico blocco, dunque l’errore generato dalla ridichiarazione della variabile "foo" usando l’operatore "let" è perfettamente corretto. Ricorda che le variabili costruite con "let" hanno block scope, dunque non possono essere ridefinite nello stesso blocco.
Per risolvere il problema, dobbiamo agire costruendo differenti blocchi, che a loro volta andranno a costituire ambienti differenti in cui l’operatore "let" diventa perfettamente legale anche con namespace identici. Per fare ciò basta avviluppare i case
in blocchi espliciti tramite l’utilizzo delle parentesi graffe { e }:
let myValue = 1;
switch(myValue) {
case 0: {
let foo;
break;
}
case 1: {
let foo;
break;
}
}
Ora la ridichiarazione è perfettamente legale.
Hoisting
Un’altra importantissima differenza tra "var" e "let" risiede nella funzionalità dell’hoisting. Con questo termine si intende il comportamento di default di Javascript in cui è possibile utilizzare una variabile prima della sua dichiarazione, che viene "spostata in alto" da parte del motore del linguaggio stesso:
x = 5; // assegnamento
elem = document.getElementById("demo"); // prendi elemento
elem.innerHTML = x; // stampa valore di x nell'elemento
var x; // dichiara x
Una variabile "var" può essere utilizzata cronologicamente prima della sua dichiarazione, e la sua dichiarazione può seguire cronologicamente il suo utilizzo.
Contrariamente, una variabile "let" produrrà un errore di tipo ReferenceError
se viene utilizzata prima della sua dichiarazione:
x = 5; // assegnamento
elem = document.getElementById("demo"); // prendi elemento
elem.innerHTML = x; // stampa valore di x nell'elemento, errore ReferenceError
let x; // dichiara x
Questo perchè a differenza delle variabili "var", che sono inizializzate dall’ambiente con il valore undefined
, le variabili "let" non vengono inizializzate fino alla loro assegnazione. Differenza sostanziale di cui è necessario essere consapevoli. La stessa situazione è certamente presente in ogni tipo di scope, anche in quello locale delle funzioni:
function do_something() {
console.log(bar); // undefined
console.log(foo); // ReferenceError
var bar = 1;
let foo = 2;
}
Le variabili "let" sono posizionate in quella che viene chiamata "Temporal Dead Zone", dall’inzio del blocco fino a quando l’inizializzazione è processata, a differenza delle variabili non dichiarate che sono tutte non-definite. Infatti, utilizzando l’operatore typeof
otteniamo lo stesso ReferenceError
:
// output: 'undefined'
console.log(typeof undeclaredVariable);
// ReferenceError'
console.log(typeof i);
let i = 10;
Conclusione
In questo articolo abbiamo fatto chiarezza sul nuovo operatore "let" e sulle motivazioni per cui questo è stato introdotto. E’ possibile bypassare i nuovi identificatori "let" e "const" ed utilizzare unicamente "var" come accadeva prima della specifica ECMA 6? Certamente. Il motore Javascript è in questo senso inalterato, e se gli script prodotti in precedenza giravano correttamente con il solo uso di "var", allora non c’è motivo di preoccuparsi ora. Tuttavia, i due nuovi identificatori non sono stati introdotti per complicare la vita dello sviluppatore, ma piuttosto per migliorarla. Il solo operatore "var" produceva dei risultati inaspettati, a causa del suo comportamento che risente dello scope globale, che necessitavano di svariati escamotage, a volte anche parecchio avanzati, per ovviare alle problematiche. Con l’introduzione di "let", abbiamo ora un controllo molto migliorato, sia sulle variabili stesse che sullo scope in cui sono prodotte ed utilizzate. Certamente la teoria, la pratica e la conoscenza dei principi fondamentali che regolano entrambi i comportamenti e delle loro differenze sostanziali è essenziale per poter utilizzare al meglio gli strumenti a disposizione e produrre codice robusto e all’avanguardia.