La maggior parte delle persone inizia con JavaScript costruendo cose. Colleghi un pulsante, recuperi dei dati e osservi il DOM cambiare. Poi, le astrazioni iniziano a mostrare i loro limiti. Emergono bug che non hanno senso: le variabili esistono prima delle loro dichiarazioni, le funzioni ricordano variabili a cui non dovrebbero avere accesso e la parola chiave this punta alla finestra (window), a un pulsante o a nulla. Di solito, è in quel momento che ti rendi conto di dover guardare sotto il cofano per capire cosa stia facendo realmente il motore.
Contesto di esecuzione: La configurazione in due fasi
Quando il motore JavaScript nel tuo browser o in Node.js incontra uno script, non si limita a leggere il file dall'alto verso il basso come una persona che scorre una pagina. Invece, crea un contesto di esecuzione, un contenitore che racchiude tutto ciò che è necessario per eseguire un particolare frammento di codice. Ogni contesto di esecuzione attraversa due fasi distinte.
Fase di creazione della memoria. Durante questo passaggio iniziale, il motore scansiona l'intero scope e alloca memoria per ogni dichiarazione di variabile e di funzione che trova. Se trova un var, riserva uno spazio e memorizza undefined come segnaposto. Se trova una dichiarazione di funzione, memorizza l'intero corpo della funzione. Questo è il motivo per cui una funzione dichiarata con la tradizionale parola chiave function può essere chiamata da righe che appaiono precedentemente nello stesso scope. Il motore ne è già a conoscenza prima di iniziare l'esecuzione.
Fase di esecuzione del codice. Ora il motore esegue il tuo codice riga per riga. Qui avvengono le assegnazioni. Le espressioni vengono valutate. Le funzioni vengono invocate. Se hai scritto var name = "Alice";, il segnaposto della prima fase viene finalmente sostituito con la stringa. Comprendere questo comportamento a due passaggi chiarisce una sorprendente quantità di dubbi. Il motore non è approssimativo; sta seguendo una rigorosa routine di configurazione.
Variabili e la Temporal Dead Zone
Scegliere tra var, let e const non è solo una preferenza stilistica. var ha uno scope di funzione, il che significa che ignora completamente le parentesi graffe. Dichiarala all'interno di un blocco if e essa trapelerà verso l'esterno. Quel comportamento potrebbe aver avuto senso nei primi tempi di JavaScript, ma nelle applicazioni moderne causa veri e propri mal di testa nella manutenzione. let e const hanno invece uno scope di blocco. Rispettano le parentesi graffe e scompaiono quando il blocco termina.
Esiste anche una differenza sottile ma cruciale nel modo in cui l'hoisting le tratta. Le dichiarazioni var vengono sollevate (hoisted) e immediatamente inizializzate con undefined. Anche let e const sono tecnicamente sollevate; il motore sa che esistono prima di raggiungere la riga della dichiarazione. Tuttavia, non vengono inizializzate. Rimangono in un limbo chiamato Temporal Dead Zone. Se provi a leggerle prima che venga eseguita la riga della dichiarazione, otterrai un errore ReferenceError invece di un subdolo undefined. Quel crash è in realtà utile: impedisce alla logica di procedere basandosi su dati non inizializzati.
Scope lessicale e Closure
Lo scope risponde a una domanda semplice: dove posso accedere a questa variabile? JavaScript utilizza lo scope lessicale, il che significa che i diritti di accesso di una funzione sono decisi da dove è stata fisicamente scritta nel codice sorgente, non da dove viene chiamata. Se definisci una funzione all'interno di un'altra funzione, quella interna può guardare verso l'esterno per leggere le variabili della sua funzione genitore. La funzione esterna non può invece guardare verso l'interno. Questa relazione è statica. Potresti passare quella funzione interna tra diversi moduli, memorizzarla in una variabile globale e chiamarla da un file completamente diverso. Ricorderà comunque le variabili dello scope in cui è nata.
Questo comportamento produce naturalmente delle closure. Una closure si forma quando una funzione interna mantiene un riferimento a una variabile del suo scope esterno. Anche dopo che la funzione esterna ha terminato l'esecuzione e le sue variabili locali dovrebbero essere state rimosse dal garbage collector, JavaScript le preserva in memoria perché la funzione interna ne ha ancora bisogno. La funzione interna porta con sé il proprio ambiente circostante.
Questo non è un semplice dettaglio accademico. Le closure ti offrono un modo pratico per creare uno stato privato in un linguaggio che manca di modificatori di accesso espliciti.
function makeCounter() {
let count = 0;
return function() {
count = count + 1;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
Qui, count è nascosto. Nulla al di fuori della funzione restituita può resettarlo o leggerlo direttamente. Si tratta di una variabile privata costruita esclusivamente tramite la meccanica dello scope.
L'hoisting nella pratica
It is common to hear that JavaScript "moves declarations to the top." That is a useful mental model, but the code does not actually get rewritten. During the memory creation phase, the engine simply registers declarations before execution begins. A statement like var x = 5; behaves as if the declaration and initialization are decoupled. The declaration var x; is processed early and initialized to undefined. The assignment x = 5; stays exactly where you wrote it and runs during the execution phase.
Because of this, var can lead to surprising results. A variable used near the top of a function might hold undefined even though a giant assignment sits at the bottom. Using let and const eliminates that particular foot-gun because the temporal dead zone forces you to keep your declarations above your usage.
How this Gets Its Value
If execution context and scope determine where variables live, this determines which object is currently in charge. Unlike lexical variables, this is not decided by where a function is written. It is decided entirely by how the function is called.
Default binding occurs when you invoke a plain, standalone function. In non-strict mode, this falls back to the global object. In a browser, that is window. Call a function without any context, and you might accidentally be touching global state without realizing it.
Implicit binding happens when you call a function as a method on an object. If you write user.sayName(), the dot quietly tells the engine to set this to user for the duration of that call. The site of the call matters more than the site of the definition.
Explicit binding lets you override everything manually. call() and apply() invoke a function immediately while forcing this to be a specific object you provide. The only difference between them is how arguments are passed: call takes a comma-separated list, while apply takes an array. bind() works differently. It does not invoke the function right away. Instead, it returns a new function with this permanently locked to the value you supplied. This is invaluable for callbacks that might otherwise lose their context when passed around.
New binding comes into play when you use the new keyword in front of a function call. The engine constructs a brand-new empty object, sets its prototype linkage, and points this inside the constructor to that fresh instance.
Writing Code That Lasts
Understanding mechanics is only half the craft. The other half is writing code that humans can read six months from now.
DRY (Don't Repeat Yourself) sounds obvious, but it is violated constantly. If you find yourself writing the same validation logic or API call pattern in three different files, extract it. Write one function. One source of truth means one place to update when requirements change, and that saves time in ways that are difficult to overstate.
KISS (Keep It Simple, Stupid) is a defense against ego. Nested ternaries and one-liner closures feel clever, but they cost hours during debugging. The closure pattern I showed earlier is powerful, yet nesting five levels deep just because you can is a mistake. Simple code survives team turnover, production incidents, and those middle-of-the-night alerts when something breaks and nobody remembers why.
The Real Payoff
Studying execution
