var

La parola chiave var serve per dichiarare una variabile globally-scoped (visibile in ogni punto del programma) o function-scoped (visibile solo dentro la funzione dentro cui è stata dichiarata).

var x; //dichiarazione senza inizializzazione, x vale undefined
var x = 1; // dichiarazione e inizializzazione insieme
var first = 0, second = "ciao"; //dichiarazione di due variabili in una sola volta

var a = 3;
function func1() {
    console.log(a); //3
}
func1();

var b = 5;
function func2() {
    b = 7; 
}
func2();
console.log(b); //7

var c = 10;
function func3() {
	// variable shadowing
    var c = 20; //la variabile dichiarata nella funzione rimane all'interno della funzione
}
func3();
console.log(c); //10 qui la variabile c è quella esterna, non quella dichiarata nella funzione

var d = 12;
if (true) {
    var d = 24; //la variabile dichiarata non in una funzione sovrascrive la precedente
}
console.log(d); //24 qui la variabile è quella ridichiarata nel blocco if

//le variabili globali sono in realtà proprietà dell'oggetto window
console.log(window.a); //3
console.log(window.b); //7
console.log(window.c); //10
console.log(window.d); //24
console.log(window.first); //0
console.log(window.second); //"ciao"
console.log(window.func1); //"ƒ func1() { console.log(a); }"
console.log(window.func2); //"ƒ func2() { b = 7; }"
console.log(window.func3); //"ƒ func3() { var c = 20; }"

let

La parola chiave let serve per dichiarare una variabile block-scoped (visibile solo dentro il blocco di parentesi graffe in cui è stata dichiarata).

let x; //dichiarazione senza inizializzazione, x vale undefined
let x = 1; // dichiarazione e inizializzazione insieme
let first = 0, second = "ciao"; //dichiarazione di due variabili in una sola volta
var pippo;
let pippo; //Uncaught SyntaxError: Identifier 'pippo' has already been declared
let pizza;
var pizza; //Uncaught SyntaxError: Identifier 'pizza' has already been declared
let casa;
let casa; //Uncaught SyntaxError: Identifier 'pizza' has already been declared

let a = 3;
function func1() {
    console.log(a); //3
}
func1();

let b = 5;
function func2() {
    b = 7;
}
func2();
console.log(b); //7

let c = 10;
function func3() {
    let c = 20; //
}
func3();
console.log(c); //10

let d = 12;
if (true) {
    let d = 24; //questa variabile non è visibile al di fuori del blocco in cui è stata dichiarata
}
console.log(d); //12

const

La parola chiave const serve per dichiarare una variabile block-scoped che non può essere riassegnata nè ridichiarata.

const x; //Uncaught SyntaxError: Missing initializer in const declaration
const x = 1; // dichiarazione e inizializzazione insieme
const first = 0, second = "ciao"; //dichiarazione di due variabili in una sola volta
var pippo = 3;
const pippo = 3; //Uncaught SyntaxError: Identifier 'pippo' has already been declared
let pizza = 3;
const pizza = 3; //Uncaught SyntaxError: Identifier 'pizza' has already been declared
const casa = 3;
const casa = 3; //Uncaught SyntaxError: Identifier 'casa' has already been declared

const a = 3;
function func1() {
    console.log(a); //3
}
func1();

const b = 5;
function func2() {
    b = 7; // Uncaught TypeError: Assignment to constant variable.
}
func2();
console.log(b); //5

const c = 10;
function func3() {
    const c = 20; //
}
func3();
console.log(c); //10

const d = 12;
if (true) {
    const d = 24; //questa variabile non è visibile al di fuori del blocco in cui è stata dichiarata
}
console.log(d); //12

//una const non è una veria e propria costante in quanto il suo valore può essere modificato
const persona = {
    nome: "Mario",
    cognome: "Rossi",
    anni: 20
}
persona.anni = 30;
console.log(persona); //{nome: "Mario", cognome: "Rossi", anni: 30}

Se non si usa nè var nè let nè const e non si sta usando lo strict-mode la variabile diventa una proprietà dell'oggetto window:

test = "ciao";
console.log(window.test); // "ciao", in strict mode ReferenceError

var one = 1;
let two = 2;
console.log(window.one); // 1
console.log(window.two); // undefined

Shadowing

Lo shadowing si verifica quando si dichiara una nuova variabile in un ambito interno che ha lo stesso nome di una variabile già esistente in un ambito esterno. Quando questo accade, la variabile più interna nasconde la variabile esterna.

let nome = "Mario";  //ambito esterno, globale
var animale = "Cane"; //ambito esterno, globale
function mostraNome() {
    let nome = "Giovanna"; 
    var animale = "Gatto";
    console.log(nome); // mostra "Giovanna" a causa dello shadowing (con let)
    console.log(animale); // mostra "Gatto" a causa dello shadowing che con var funziona nell'ambito di funzione

}
mostraNome();
console.log(nome); // mostra "Mario" perchè qui siamo fuori dall'ambito della funzione mostraNome, la variabile let locale è distrutta al di fuori della funzione
console.log(animale); // mostra "Cane" perchè qui siamo fuori dall'ambito della funzione mostraNome, la variabile var locale è distrutta

let counter = 10;
var year = 1982;
var altezza = 157; 
if (true) {
    counter = 20; // qui non stiamo dichiarando un'altra variabile ma stiamo accedendo a quella esterna
    year = 2025;  // qui non stiamo dichiarando un'altra variabile ma stiamo accedendo a quella esterna
    var altezza = 190; //var ignora lo scope di blocco e di fatto sovrascrive la variabile esterna 'altezza'. Non avviene shadowing. A causa dell'hoisting la dichiarazione della var viene spostata all'inizio dell'ambito globale (o della funzione se fossimo dentro una funzione invece che in un blocco if
    console.log(counter); // mostra 20
    console.log(year); // mostra 2025
    console.log(altezza); // mostra 190
}
console.log(counter);  // mostra 20 perchè abbiamo modificato la variabile esterna
console.log(year);  // mostra 2025 perchè abbiamo modificato la variabile esterna
console.log(altezza);  //mostra 190 perchè la dichiarazione interna con var ha sovrascritto quella esterna, in quanto var non ha scope di blocco.

Hoisting

E' importante dichiarare in un modo o nell'altro le var prima di utilizzarle. L'utilizzo di una var non dichiarata non provoca errori perchè le var sono memorizzate prima che il codice sia processato ma con valore undefined (hoisting). Le let e le const invece non possono essere utilizzate prima di essere dichiarate. Le funzioni invece possono essere utilizzate prima della loro dichiarazione a meno che queste non siano assegnate ad una var, in quel caso sempre a causa dell'hoisting la variabile vale undefined.

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

console.log(b); //undefined
var b = 3;

console.log(c); //Uncaught ReferenceError: c is not defined
let c = 5;

console.log(d); //Uncaught ReferenceError: Cannot access 'd' before initialization
const d = 10;

func1(); //è possibile utilizzare una funzione prima di averla dichiarata
function func1() { //function declaration
    console.log("func1"); //"func1"
}

func2(); //Uncaught TypeError: func2 is not a function in questo momento la variabile func2 vale undefined
var func2 = function() { //function expression
    console.log("func2");
}

L'hoisting avviene perchè l'esecuzione di un programma JavaScript avviene in due fasi, prima il parsing e poi l'esecuzione.