Scope e Hoisting in JavaScript: closure, shadowing e cattura dello stato

by Anastasia P.
SHARE
Scope e Hoisting in JavaScript: closure, shadowing e cattura dello stato
© Guida-HTML5.it

Introduzione

Quando una funzione JavaScript viene definita dentro un’altra funzione, può continuare ad accedere alle variabili del contesto esterno anche dopo che la funzione esterna ha terminato la propria esecuzione. Questo comportamento si basa sullo scope lessicale e prende il nome di closure.

Le closure sono molto utili per creare contatori, configurazioni private, generatori di funzioni e gestori di stato. Tuttavia, scope e hoisting possono rendere il comportamento meno intuitivo, soprattutto quando una funzione interna usa variabili con lo stesso nome di quelle esterne oppure quando una dichiarazione viene spostata apparentemente in alto dal meccanismo di hoisting.

In questo tutorial realizzeremo un piccolo sistema per creare contatori indipendenti. Analizzeremo:

  • come una closure conserva l’accesso allo scope esterno;
  • come funziona la catena degli scope;
  • come il shadowing nasconde una variabile esterna;
  • quale ruolo svolge l’hoisting nelle funzioni utilizzate dalla closure;
  • come evitare errori legati a var, let e const.

Codice completo

// Crea un contatore indipendente per ogni chiamata
function creaContatore(nome, valoreIniziale = 0) {
  // Questa variabile appartiene allo scope della funzione esterna
  let valore = valoreIniziale;

  // Dichiarazione di funzione: è soggetta a hoisting
  function incrementa() {
    valore += 1;
    return valore;
  }

  function leggiValore() {
    return valore;
  }

  function descrivi() {
    // Questa variabile locale fa shadowing del parametro "nome"
    const nome = "Contatore interno";
    return `${nome}: valore corrente = ${valore}`;
  }

  // Le funzioni interne mantengono un riferimento
  // alle variabili dello scope di creaContatore
  return {
    incrementa,
    leggiValore,
    descrivi
  };
}

const contatoreA = creaContatore("A", 10);
const contatoreB = creaContatore("B", 100);

console.log(contatoreA.leggiValore()); // 10
console.log(contatoreB.leggiValore()); // 100

console.log(contatoreA.incrementa());  // 11
console.log(contatoreA.incrementa());  // 12

console.log(contatoreB.incrementa());  // 101

console.log(contatoreA.leggiValore()); // 12
console.log(contatoreB.leggiValore()); // 101

console.log(contatoreA.descrivi());
// Contatore interno: valore corrente = 12

// Esempio di hoisting di una dichiarazione di funzione
mostraMessaggio();

function mostraMessaggio() {
  console.log("La funzione è stata dichiarata più in basso.");
}

// Errore: "dati" è in una Temporal Dead Zone fino alla dichiarazione
// console.log(dati);
// const dati = { attivo: true };

Spiegazione

La closure conserva lo scope esterno

Quando viene eseguita creaContatore(), viene creata la variabile valore. Normalmente si potrebbe pensare che questa variabile venga eliminata al termine della funzione. In realtà, le funzioni incrementa, leggiValore e descrivi la utilizzano ancora.

JavaScript mantiene quindi disponibile lo scope necessario alla closure. Non viene conservata necessariamente l’intera funzione esterna, ma l’ambiente lessicale contenente i dati ancora raggiungibili. Per questo contatoreA e contatoreB possiedono due valori distinti: ogni chiamata a creaContatore() genera un nuovo ambiente.

La catena di ricerca è la seguente:

  • JavaScript cerca una variabile nello scope della funzione che sta eseguendo;
  • se non la trova, controlla lo scope esterno;
  • continua risalendo la catena degli scope;
  • se non trova alcuna dichiarazione, produce un ReferenceError.

Quando incrementa() utilizza valore, non cerca una nuova variabile locale: risale allo scope di creaContatore() e modifica quella catturata dalla closure.

Shadowing: una variabile può nasconderne un’altra

La funzione creaContatore riceve il parametro nome. Dentro descrivi(), però, viene dichiarata una nuova costante chiamata ancora nome:

function descrivi() {
  const nome = "Contatore interno";
  return nome;
}

Questa dichiarazione crea una variabile nello scope di descrivi. La variabile locale ha lo stesso nome del parametro esterno e lo nasconde: questo comportamento è chiamato shadowing.

Lo shadowing non modifica la variabile esterna. Semplicemente, all’interno dello scope più vicino, il nome viene associato alla variabile locale. Per rendere il codice più leggibile, è consigliabile evitare nomi uguali in scope vicini, soprattutto in funzioni lunghe o complesse.

Hoisting delle dichiarazioni di funzione

La chiamata a mostraMessaggio() appare prima della dichiarazione testuale della funzione. Il codice funziona perché le dichiarazioni di funzione tradizionali vengono inizializzate durante la fase di preparazione dell’ambiente di esecuzione.

Questo comportamento è diverso da quello di una funzione assegnata a una variabile:

// Non funziona: la costante non è inizializzata
// eseguiOperazione();

const eseguiOperazione = function () {
  console.log("Operazione eseguita");
};

In questo caso viene hoistata la dichiarazione della variabile, ma la sua inizializzazione avviene solo quando l’esecuzione raggiunge la riga dell’assegnazione. Prima di quel punto la costante si trova nella Temporal Dead Zone.

Perché preferire let e const

let e const hanno scope di blocco e non possono essere utilizzate prima della dichiarazione. Questo rende più visibili gli errori di ordine e riduce i problemi tipici di var, che ha scope di funzione e viene inizializzata con il valore undefined.

function esempioVar() {
  console.log(numero); // undefined
  var numero = 5;
}

function esempioLet() {
  // ReferenceError: numero è nella Temporal Dead Zone
  // console.log(numero);
  let numero = 5;
}

Best practice

  • Usa const per riferimenti che non devono essere riassegnati e let quando il valore deve cambiare.
  • Evita var nel codice moderno, salvo la necessità di gestire codice legacy.
  • Nomina le variabili in modo esplicito per ridurre lo shadowing involontario.
  • Usa le closure per nascondere lo stato interno, senza esporre direttamente variabili modificabili dall’esterno.
  • Non fare affidamento sull’hoisting per organizzare l’ordine del codice: dichiara funzioni e variabili prima del loro utilizzo quando possibile.
  • Ricorda che ogni chiamata a una funzione che crea una closure può produrre un ambiente indipendente.
  • Quando una closure viene conservata a lungo, verifica che non mantenga inutilmente riferimenti a oggetti molto grandi.

Riepilogo

Scope e hoisting influenzano direttamente il funzionamento delle closure. Una funzione interna può mantenere l’accesso alle variabili dello scope esterno, creando uno stato privato e persistente. La ricerca dei nomi segue la catena degli scope, partendo dal contesto più vicino e risalendo verso quelli esterni.

Quando una variabile locale ha lo stesso nome di una variabile esterna si verifica lo shadowing. La variabile esterna non viene eliminata, ma non è direttamente visibile nello scope interno. Le dichiarazioni di funzione tradizionali sono disponibili prima della loro posizione nel codice grazie all’hoisting, mentre le funzioni assegnate a let o const possono essere utilizzate solo dopo l’inizializzazione.

Approfondisci con risorse ufficiali

SHARE