So di non sapere JavaScript – Scope e Closures

Una delle frasi che hanno reso famoso il filosofo greco Socrate era: “Io so di non sapere”. Sicuramente non si riferiva a JavaScript, ma è il giusto approccio da tenere davanti a questo linguaggio che tutti noi crediamo di conoscere senza averlo mai studiato. Ebbene, dopo aver letto alcuni volumi della serie “You don’t know JavaScript“, posso dire di aver scoperto molte cose che non sapevo, come per esempio, udite udite, che JS è compilato e non interpretato!

You don’t know JavaScript è una serie di libri sui concetti base che stanno dietro all’uso quotidiano di costrutti JavaScript che spesso usiamo quasi in modo automatico, senza averne piena padronanza. In particolare, in due piccoli volumi che ho avuto modo di leggere vengono spiegati gli scope, le closure, la famigerata parola chiave this e il prototype. Con questo primo post voglio semplicemente riorganizzare le idee e raccogliere le best practices emerse durante la lettura sui primi due concetti.

La prima volta non si scorda mai

La prima volta che comprai un manuale di Java (nel lontano 2005), un mio amico mi disse: “Che fai? Ti metti a studiare JavaScript?” Se a quel tempo fosse esistito “You don’t know JavaScript” e avessi letto la prefazione, sicuramente avrei risposto: “JavaScript sta a Java come Carnival sta a Car”! Ed è verissimo, la somiglianza dei nomi è puramente una scusa politico/commerciale che fa apparire JavaScript come il fratello minore di Java, quando in realtà in comune hanno solo un po’ di sintassi (quella mutuata dal C…). Fino a qualche anno fa si poteva almeno dire che uno era un linguaggio ad oggetti lato server, l’altro un linguaggio di scripting lato client: oggi non è più propriamente vero nemmeno questo… quindi? C’è voluto Node.js affinché JavaScript conquistasse la sua autonomia? Probabilmente si, e improvvisamente tutti (compreso il sottoscritto) cominciano a prenderlo sul serio e a studiarlo.

Scope

Una delle traduzioni di “scope” può essere “ambito” e probabilmente è quella più azzeccata per descrivere l’argomento.
Di che ambito si sta parlando? In JavaScript, per scope si intende l’ambito di visibilità delle variabili, ovvero quell’insieme di regole che determina come vengono create e come e dove si recuperano. Una delle cose che mi hanno stupito è proprio il fatto che JavaScript è un linguaggio compilato e non interpretato! Ovviamente non nel senso classico del termine, ma il compilatore esegue diverse fasi di lettura sul codice in modo da capirne l’albero sintattico, farne il parsing e dare una semantica alle istruzioni convertendole in codice macchina, permettendosi anche delle ottimizzazioni in tempo reale: si può dire quindi che è compilato un attimo prima di essere eseguito!

Come fa l’autore a convincerci che sta dicendo il vero? Facciamo parlare il codice

sayHello();

function sayHello() {
   console.log("Hello World");
}

In un linguaggio interpretato conta l’ordine di dichiarazione delle variabili affinché si possano usare: in questo caso, la funzione sayHello è usata prima della dichiarazione: questo significa che è successo qualcosa prima dell’esecuzione (in particolare, l’operazione di “sollevare” le dichiarazioni delle variabili e delle funzioni prima dell’esecuzione è definita “hoisting“).

Semplifichiamo un po’: quando quindi scriviamo:

var a = 2;

Che succede dietro le quinte?

  • Il compilatore incontra var a e chiede allo scope se questa variabile esiste già: se si, ignora l’istruzione, altrimenti la istanzia nello scope.
  • Il compilatore produce il codice macchina per assegnare 2 ad a.
  • Il motore JS chiede allo scope se esiste la variabile a: in caso affermativo assegna 2, altrimenti chiede allo scope padre, fino al livello “global“. Se anche qua la variabile non viene trovata si solleva un errore.

La ricerca a ritroso delle variabili nei vari scope viene detta “look up” ed è rappresentata come in figura:

Se invece scrivo

a = 2;

si tratta di una dichiarazione implicita di una variabile che, se non siamo in strict mode, viene associata allo scope “globale”, altrimenti viene lanciata una ReferenceError in fase di assegnazione perché la variabile non esiste.

La vera domanda a questo punto è: dove comincia e dove finisce uno scope?! L’esempio riportato è molto eloquente:

Ogni area colorata corrisponde ad uno scope, cioè al corpo di una funzione!! Tutto qua quindi?! Perché parlare sempre di scope e non direttamente di funzioni? Perché appunto non è tutta qua…
A differenza di altri linguaggi, come Java per esempio, lo scope di una variabile è delimitato a livello di funzione e non a livello di blocco di codice tra graffe: ovviamente fatta la legge, trovato l’inganno, perché questa affermazione è vera al 90%, ma andiamo per gradi. Nel seguente frammento di codice:

function hello() {
   for (var i=0; i<3; i++) { 
      console.log("In loop " + i);
   }
   console.log("Out of loop " + i);
}

Che output vi aspettate alla riga evidenziata? Ecco l’output:

In loop 0
In loop 1
In loop 2
Out of loop 3

Inaspettatamente i è ancora valida fuori dal ciclo for, perché lo scope in questo caso è a livello funzione, non a livello di blocco come siamo abituati!! Se non si ha ben chiara questa situazione si può incorrere in errori difficili da individuare.

Quel 10% di eccezioni rispetto alla regola invece? Gli unici casi in cui si ha uno scope a livello di blocco tra graffe sono:

  • quando si usa with (che comunque è deprecato)
  • nel blocco catch di una istruzione try/catch (da ES3)
  • da ES6, usando il modificatore let o const al posto di var, si permette la creazione di uno scope nel blocco in cui si trova la variabile. Al momento ES6 è ancora in draft e anche il supporto ai browser è limitato. Chrome per esempio ammette questi operatori solo in “strict mode” (al momento della scrittura di questo post).

In ES6, possiamo scrivere:

function hello() {
   "use strict";
   for (let i=0; i<3; i++) { 
      console.log("In loop " + i);
   }
   console.log("Out of loop " + i);
}

e ottenere il comportamento più familiare:

In loop 0
In loop 1
In loop 2
Uncaught ReferenceError: i is not defined

E prima di ES6? Come possiamo isolare il ciclo for e non sporcare lo scope con la variabile i? Isolando per esempio il ciclo in un altro scope annidato, ovvero in una funzione, o meglio una cosiddetta function expression, dichiarata ed eseguita:

function hello() {
   (function loop() {
      for (var i=0; i<3; i++) { 
         console.log("In loop " + i);
      }
   })()
   console.log("Out of loop " + i);
}

Una function expression è una funzione contenuta tra parentesi tonde, sintatticamente valida come normale espressione. Le altre due parentesi alla riga 6 invece eseguono l’espressione: questo pattern è noto con il nome di Immediately Invoked Function Expression (IIFE).

Un’ultima nota: l’espressione poteva essere benissimo una funzione anonima, ma è sempre bene evitarle e dare un nome alle funzioni, sia per scopo semantico e di leggibilità del codice, ma soprattutto per semplificarci la vita in fase di debug e lettura degli stacktrace, spesso incomprensibili. Incapsulare il codice in espressioni IIFE è una buona pratica da tenere sempre a mente, in primis perché soddisfa il principio di “information hiding” che conosciamo bene nella programmazione ad oggetti, ma soprattutto ci evita la possibile collisione tra nomi, visto che non esistono package… L’architettura a moduli di AngularJS per esempio non è niente di magico: è esattamente l’applicazione di questi principi.

Scope e performance

Lo scope a cui si fa riferimento in JavaScript è di tipo statico, definito anche come lexical scope, perché, durante la fase di compilazione, viene data una semantica alle istruzioni scritte da noi in fase di stesura del codice (detto author time). Questi due termini ritornano molto spesso nei capitoli che riguardano lo scope; riassumendo:

  • author time: momento della stesura del codice in cui si definiscono le variabili e i loro ambiti;
  • lexical scope: è lo scope di tipo statico in cui il compilatore dà semantica su quanto scritto in author time

L’autore dà molta enfasi all’author time e allo scope lessicale da esso generato perché è in contrasto con quanto accade, per esempio, se si usa la funzione eval: ci hanno insegnato a non usarla e a considerarla il male assoluto, ma è bene conoscere il nemico per saperlo sconfiggere.

Esistono di fatto due meccanismi per fregare il lexical scope a runtime e “scongelare” quanto definito ad author time: uno è appunto la funzione eval, l’altro è la parola chiave with, dimenticata nel tempo e considerata ormai deprecata. eval invece ogni tanto ce lo troviamo con orrore tra i piedi, ma perché dobbiamo temerlo?

Cosa fa eval? In pratica prende come argomento una stringa e la tratta come se fosse codice da eseguire: subito ci scatta un campanello d’allarme perché ci possono essere potenziali problemi di sicurezza, visto che può eseguire qualcosa che non ho scritto ad author time, ma non è solo quello il problema. Dal momento che il suo contenuto viene valutato a runtime, il motore javascript non è capace di applicare nessun meccanismo di ottimizzazione su quel codice, anzi: tutto il blocco di codice che contiene un eval non viene sottoposto a nessuna ottimizzazione perché, non sapendo cosa esso conterrà, l’ottimizzazione probabilmente sarebbe inutile. Risultato: ho un codice meno sicuro e più lento. Credo di non dover aggiungere altro.

Le Closures

Le closures probabilmente le abbiamo usate tutti in modo inconsapevole e automatico, senza sapere che si chiamassero così. Diamo una definizione:

Una “chiusura” (closure) è quello che una funzione è capace di ricordare del contesto (lexical scope) dalla quale proviene, anche se viene eseguita al di fuori di esso.

Aggiungiamo anche un esempio per capire meglio:

function foo() { 
   var a = 2;
   function bar() { 
      console.log( a );
   }
   return bar; 
}
var referenceToBar = foo();
referenceToBar(); // output: 2

Secondo la definizione, la funzione bar fa chiusura sul contesto lessicale a cui appartiene, ovvero il corpo della funzione foo, e si ricorda di quello scope perché, eseguita esternamente ad esso tramite referenceToBar(), produce 2 come output, che era il valore di a nel contesto originale.

La funzione bar quindi, secondo il criterio di look up tra gli scope definito in precedenza, risolve la variabile a e il suo valore, (memorizzato forse nello stack delle chiamate?): fin qua niente di nuovo. La sorpresa è che il riferimento a bar viene portato fuori dal lexical scope in cui è stata definita, ricordandosi dello stato a cui aveva accesso in quel momento: tramite il legame di riferimenti referenceToBar -> bar -> a, quest’ultimo valore non viene cancellato dal garbage collector perché qualcuno lo sta ancora usato. La chiusura quindi è data dal fatto di avere accesso ad uno scope tenuto in vita grazie ad un riferimento ad esso.

Credete di non averlo mai usato inconsapevolmente? Sarà capitato spesso di scrivere un codice di questo tipo:

function showAlert(message) {
   setTimeout(function timer(){ 
      alert(message);
   }, 1000);
}

showAlert("This is a closure!!");

Ebbene, la chiamata ad alert con il messaggio fornito in ingresso avviene automaticamente dopo 1 secondo, quindi al di fuori del contesto nella nostra chiamata a showAlert, eppure il messaggio è quello corretto! L’esecuzione della callback timer da parte di setTimeout segue lo stesso principio!

O ancora, usando jQuery non avete mai scritto:

function setupButton(name, selector) {
   $(selector).click(function activator() {
      console.log("Activating: " + name); 
   });
}

setupButton("Closure 1", "#button-1" );
setupButton("Closure 2", "#button-2" );

Ogni closure memorizzerà il valore di name giusto per ogni bottone. In generale quindi, ogni volta che abbiamo a che fare con callback, le closure entrano sempre in gioco.

Closure e moduli

Il pattern più elegante che possiamo implementare al momento e che sfrutta le closures (come fa AngularJS), è il modulo. In cosa consiste un modulo JavaScript? Un esempio vale più di mille parole:

function CoolModule() {

        var something = "cool"; 
        var another = [1, 2, 3];

        function doSomething() { 
                console.log(something);
        }
        
        function doAnother() {
                console.log(another.join("!"));
        }
        
        return {
                doSomething: doSomething, 
                doAnother: doAnother
        };
}

var foo = CoolModule(); // Creo il modulo
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

Niente di diverso rispetto a quello che abbiamo visto poco fa: abbiamo una funzione CoolModule che espone due funzioni definite internamente (doSomething, doAnother) perché ritorna un oggetto JavaScript che fa riferimento ad esse, le quali hanno accesso allo scope interno alla funzione (something, another), anche se vengono eseguite al di fuori di esso! Con qualche accorgimento in più, non ricordano proprio i moduli di AngularJS?? E’ così infatti che possiamo definire una API pubblica: foo infatti può avere accesso alle funzioni doSomething e doAnother, ma non alle variabili interne a CoolModule. Come abbiamo già detto, questo ci permette di creare una sorta di namespace per evitare collisioni tra nomi e non sovraffollare lo scope di variabili, nascondendo ciò che vogliamo mantenere privato.

A questo punto sorge un dubbio: che differenza c’è dall’usare una constructor function? Qui si aprirebbe una discussione tra la parola chiave this e il prototype, che è argomento del prossimo post!

Conclusioni

Cosa mi è rimasto dopo la lettura di questo libro? Sicuramente il riassunto e la riorganizzazione delle idee in questo post 🙂 . Apparte gli scherzi, la rivelazione più “scioccante” è stata scoprire che JavaScript fosse compilato: nell’uso del linguaggio si intuisce che viene effettuato qualche passaggio sul codice prima della sua esecuzione, (basti pensare a quando magari incorriamo in errori sintattici, il motore JS ci avverte subito, non al momento dell’esecuzione del codice), ma che si potesse parlare veramente di compilazione con fasi di ottimizzazione non me lo sarei aspettato.

L’altro concetto fondamentale da ricordare è il livello di visibilità e annidamento degli scope che non coincide quasi mai con i blocchi di codice tra graffe, ma bensì alle funzioni, nella maggior parte dei casi. Tenendo a mente questo concetto, il module pattern è sicuramente una tecnica da usare per scrivere un codice più strutturato, meno soggetto a collisioni tra nomi e semanticamente più chiaro.

Andrea Como

Sono un software engineer focalizzato nella progettazione e sviluppo di applicazioni web in Java. Presso OmniaGroup ricopro il ruolo di Tech Leader sulle tecnologie legate alla piattaforma Java EE 5 (come WebSphere 7.0, EJB3, JPA 1 (EclipseLink), JSF 1.2 (Mojarra) e RichFaces 3) e Java EE 6 con JBoss AS 7, in particolare di CDI, JAX-RS, nonché di EJB 3.1, JPA2, JSF2 e RichFaces 4. Al momento mi occupo di ECM, in particolar modo sulla customizzazione di Alfresco 4 e sulla sua installazione con tecnologie da devops come Vagrant e Chef. In passato ho lavorato con la piattaforma alternativa alla enterprise per lo sviluppo web: Java SE 6, Tomcat 6, Hibernate 3 e Spring 2.5. Nei ritagli di tempo sviluppo siti web in PHP e ASP. Per maggiori informazioni consulta il mio . - -

  • Federico Locci

    Ciao Andrea, un ottimo articolo, mi hai chiarito alcuni dubbi che avevo. Grazie

  • Stefano Vollono

    Veramente un ottimo articolo.