JavaScript: cosa sono lo “Scope” e il “Context”

In JavaScript, l’implementazione dello “scope” e del “context” è una caratteristica peculiare del linguaggio, in parte perché è un aspetto molto flessibile.
Le funzioni possono essere adottate per molti “context”, e gli “scope” (la ‘portata’, o meglio, l’ambito della funzione) possono essere incapsulati e conservati.
Questi concetti si prestano ad alcuni dei più potenti modelli di progettazione che JavaScript ha da offrire.
Tuttavia, essi rappresentano anche una straordinaria fonte di confusione tra gli sviluppatori, e per buoni motivi!
Il seguente articolo è una traduzione autorizzata dall’autore del post “Understanding Scope and Context in JavaScript”: una spiegazione esauriente dello “scope” e del “context”, che ne esamina le differenze, e affronta come i vari “design patterns” ne fanno uso.

Contesto vs Scope

La prima cosa importante da chiarire è che il “context” e lo “scope” di una funzione non sono la stessa cosa.
Molti sviluppatori confondono i due termini, descrivendo in modo errato uno per l’altro, anche perché la terminologia è diventato abbastanza confusa nel corso degli anni.

Ogni chiamata di funzione ha sia un “ambito” (lo ‘scope’), sia un “contesto” (il ‘context’) associati alla funzione.

Fondamentalmente, lo “scope” è basato sulle funzioni mentre il “context” è basato sulla componente a oggetti della funzione.

In altre parole, lo ‘scope’ riguarda la l’accesso a una variabile dentro a una funzione, quando essa viene chiamata: lo scope è unico per ogni chiamata. Il ‘context’ è sempre il valore della parola chiave “this”, che è un riferimento all’oggetto che “possiede” il codice attualmente in esecuzione.

Lo ‘Scope’ delle variabili

Una variabile può essere definita in ambito locale o globale, il che stabilisce l’accessibilità delle variabili dai diversi ambiti durante il ‘runtime’.

Scope delle Varibili Globali

Qualsiasi variabile definita in ‘ambito’ (‘scope’) globale, cioè una variabile dichiarata al di fuori del corpo di una funzione, vivrà in tutto il runtime, e può essere consultato e modificato in qualsiasi ambito.

Scope delle Varibili Locali

Le variabili locali esistono solo dentro al corpo della funzione all’interno della quale sono definite, e avranno una ‘portata’, un ambito diverso per ogni chiamata di tale funzione. All’interno di esse, sono soggetta all’assegnazione di valore, al loro recupero, e alla manipolazione solo all’interno di questa chiamata, e non sono accessibili al di fuori di tale ambito.

JavaScript attualmente non supporta lo scoping a livello di blocco, che è la capacità di definire una variabile nel campo di applicazione di un’istruzione if, di uno switch , o di un ciclo for, o while.

Se lo fosse, significherebbe che la variabile non sarebbe accessibile al di fuori dell’apertura e chiusura parentesi graffe del blocco. Attualmente tutte le variabili definite all’interno di un blocco sono accessibili al di fuori del blocco. Tuttavia, questo sta per cambiare, la parola chiave let è stata ufficialmente aggiunta alla specifica ES6. Può essere utilizzata in alternativa per la parola chiave “var”, e sostiene la dichiarazione delle variabili locali di ambito blocco .

Giusto per completezza sul tema delle variabili, ecco la differenza tra variabile globale e proprietà dell’oggetto ‘window’.

Quale è il contesto di ‘this’? Il ‘context’!

Il “contesto” del ‘this’ è spesso determinato da come viene richiamata una funzione. Quando una funzione viene richiamata come metodo di un oggetto, il ‘this’ viene impostata sull’oggetto che ‘possiede’ il metodo che è chiamato. Ecco sotto un esempio:

var literalCustomObj = {
    internalMethod: function(){
        alert(this === literalCustomObj);
        // Il 'this' è l'oggetto,
        // non la funzione
    }
};

literalCustomObj.internalMethod(); // true

Lo stesso principio vale quando si richiama una funzione con l’operatore ‘new’, per creare un’istanza di un oggetto. Quando viene richiamato in questo modo, il valore di ‘this’ nell’ambito della funzione verrà impostato all’istanza appena creata:

function literalFunction(){
    alert(this);
}

literalFunction() // window
new literalFunction() // literalFunction

Quando viene chiamato all’interno funzione non associata a nulla, il ‘this’ verrà impostato nel contesto globale, cioè l’oggetto ‘window’ del browser. Tuttavia, se la funzione viene eseguita in modalità strict mode, il contesto sarà di default ‘undefined’.

Contesto di esecuzione

JavaScript è un linguaggio a thread unico, il che significa che, in un dato momento, solo una cosa può essere fatta nel browser.
Quando l’interprete JavaScript esegue inizialmente il codice, entra in primo luogo in un contesto di esecuzione globale per impostazione predefinita. Ogni chiamata di una funzione da questo punto in determinerà la creazione di un nuovo contesto di esecuzione.

Questo è il punto dove si crea la confusione, il termine “contesto di esecuzione” è in realtà a tutti gli effetti riferito più allo ‘scope’ e non al contesto, come discusso in precedenza. Si tratta di una convenzione di denominazione un po’ sfortunato, ma è la terminologia definita dalle specifiche ECMAScript, quindi dobbiamo prenderne atto e usarla in questo modo.

Ogni volta che viene creato un nuovo contesto di esecuzione, viene aggiunto alla parte superiore dello stack di esecuzione. Il browser eseguirà sempre il contesto di esecuzione corrente, che è in cima alla stack di esecuzione. Una volta completato, verrà rimosso dalla parte superiore della pila, e per poi tornare al contesto di esecuzione successivo.

Un contesto di esecuzione può essere diviso in una fase di creazione e una di esecuzione. Nella fase di creazione, l’interprete prima crea un oggetto variabile ( chiamato anche un oggetto di attivazione ) che è composto di tutte le variabili, le dichiarazioni di funzione e gli argomenti definiti all’interno del contesto di esecuzione. Da lì la catena dell’ambito (‘scope chain’) viene successivamente inizializzata, e il valore di ‘this’ è determinato per ultimo. Poi nella fase di esecuzione, il codice viene interpretato ed eseguito.

The Scope Chain: la catena di esecuzione dell’ambito

Per ogni contesto di esecuzione (execution context), c’è una catena dello ‘scope’ accoppiata ad esso. La catena dello ‘scope’ contiene l’oggetto variabile per ogni contesto di esecuzione nello stack di esecuzione. Viene utilizzato per determinare l’accesso della variabile per la risoluzione dell’identificatore. Per esempio:

function first(){
    second();
    function second(){
        third();
        function third(){
            fourth();
            function fourth(){
                // do something
            }
        }
    }   
}
first();

L’esecuzione del codice precedente si tradurrà nelle funzioni nidificate che si stanno eseguendo, dalla prima fino alla quarta funzione. A questo punto, le catene di esecuzione dello scope, dall’alto verso il basso, saranno: fourth(), third(), second(), first(), globale. La quarta funzione avrà accesso alle variabili globali e alle eventuali variabili definite all’interno della prima, seconda, e terza funzione, nonché le funzioni stesse.

I conflitti di nomi tra le variabili tra i diversi contesti di esecuzione vengono risolti salendo la catena dello scope, spostandosi dal livello locale a quello globale. Ciò significa che le variabili locali con lo stesso nome delle variabili più in alto nella catena dello scope hanno la precedenza.

Riassumendo, ogni volta che si tenta di accedere a una variabile nel contesto di esecuzione di una funzione, il processo di look-up inizierà sempre con il proprio oggetto variabile. Se l’identificatore non viene trovato nell’oggetto variabile, la ricerca continua verso l’alto nella catena dello scope. La ricerca salirà infatti la catena dello scope per esaminare l’oggetto variabile all’interno di ogni contesto di esecuzione, alla ricerca di una corrispondenza per il nome della variabile.

Le ‘closure’: cosa sono?

Una ‘closure’ (che significa chiusura) si forma quando una funzione nidificata è resa accessibile all’esterno della funzione in cui è stata definita, in modo che possa essere eseguita dopo l’esecuzione della funzione esterna.
Essa mantiene l’accesso alle variabili locali, agli argomenti e alle dichiarazioni di funzioni interne della sua funzione esterna. L’incapsulamento permette di nascondere e preservare il contesto di esecuzione da ambiti esterni, esponendo un’interfaccia pubblica: la ‘closure’ è quindi soggetta a ulteriori manipolazioni. Un semplice esempio potrebbe essere:

function foo(){
    var local = 'Sono una variabile privata';
    return function bar(){
        return local;
    }
}
// Assegno a una varibile la funzione foo
var getLocalVariable = foo();
getLocalVariable() 
// Eseguo la funzione che mi rende accessibile 
// 'private variable'

Uno dei più popolari tipi di ‘closure’ è quello che ampiamente conosciuto come il ‘module pattern: esso permette di emulare i membri pubblici, privati ​​e privilegiati:

var Module = (function(){
    var privateProperty = 'foo';
    
    function privateMethod(args){
        // Fai qualcosa!
    }

    return {

        publicProperty: '',

        publicMethod: function(args){
            // Fai qualcosa!
        },

        privilegedMethod: function(args){
            privateMethod(args);
        }
    }
})();

Il pattern a modulo si comporta come se fosse un singleton, che viene eseguito appena il compilatore lo interpreta, quindi dall’apertura fino alla chiusura della parentesi, alla fine della funzione. Gli unici membri disponibili al di fuori del contesto di esecuzione della ‘closure’ sono i metodi e le proprietà pubblici, situate nell’oggetto di ritorno (‘Module.publicMethod’, per esempio).

Tuttavia, tutte le proprietà e metodi privati ​​vivranno per tutta la durata dell’applicazione, mentrè è preservato il contesto di esecuzione, e ciò significa che le variabili sono soggette ad ulteriori interazione tramite i metodi pubblici.

Un altro tipo di ‘closure’ è quello che viene chiamato un'”espressione di funzione invocata immediatamente (Iife)”, che non è altro che una funzione anonima che si auto-chiama, eseguita nel contesto dell’ogetto window:

(function(window){
          
    var a = 'foo',
        b = 'bar';
    
    function private(){
        // do something
    }
        
    window.Module = {
        
        public: function(){
            // do something 
        }
    };

})(this);

Questa espressione è molto utile quando si tenta di preservare il ‘namespace’ globale, così tutte le variabili dichiarate all’interno del corpo della funzione saranno locali nei confronti della ‘closure’, ma saranno ancora vive per tutto il ‘runtime’. Questo è un modo molto usato per incapsulare codice sorgente nelle applicazioni e nei frameworks, che espongono in genere una singola interfaccia globale in cui interagire.

I metodi ‘Call’ e ‘Apply’

Questi due metodi semplici sono inerenti a tutte le funzioni, e consentono di eseguire qualsiasi funzione in qualsiasi ‘contesto’ si desideri. Il metodo ‘call’ richiede che gli argomenti siano elencati esplicitamente, mentre il metodo ‘apply’ consente di fornire gli argomenti come un array. Il primo argomento di entrambe è il contesto:

function user(first, last, age){
    // do something 
}
user.call(window, 'John', 'Doe', 30);
user.apply(window, ['John', 'Doe', 30]);

Il risultato di entrambe le chiamate è esattamente lo stesso, la funzione ‘user’ viene richiamata nel contesto dell’oggetto globale ‘window’, e gli vengono passati gli stessi tre argomenti.

ECMAScript 5 (ES5) ha introdotto il metodo ‘Function.prototype.bind’, che viene utilizzato per manipolare il ‘context’. Esso restituisce una nuova funzione che è legato in modo permanente al primo argomento di ‘bind’, a prescindere da come viene utilizzata la funzione. Esso funziona utilizzando una ‘closure’ che è responsabile di reindirizzare la chiamata nel contesto appropriato. Vedere il seguente polyfill per i browser supportati:

if(!('bind' in Function.prototype)){
    Function.prototype.bind = function(){
        var fn = this, context = arguments[0], args = Array.prototype.slice.call(arguments, 1);
        return function(){
            return fn.apply(context, args.concat(Array.prototype.slice.call(arguments)));
        }
    }
}

È comunemente usato quando il contesto è perduto; nella programmazione ‘object-oriented’ e nella gestione degli eventi. Ciò si rende necessario perché il metodo ‘addEventListener’ di un nodo esegue sempre la funzione di ‘callback’ nel contesto del nodo del gestore degli eventi cui è associato. Tuttavia, se il si stanno sviluppando tecniche avanzate orientate agli oggetti, e si richiede che la callback sia un metodo di un’istanza, si dovrà regolare manualmente il ‘context’, ed è qui che ‘bind’ viene in aiuto:

function MyClass(){
    this.element = document.createElement('div');
    this.element.addEventListener('click', this.onClick.bind(this), false);
}

MyClass.prototype.onClick = function(e){
    // Esegue qualcosa
};

Mentre controllavate la fonte della funzione ‘bind’, potreste aver notato anche che quello che sembra essere una relativamente semplice riga di codice, preveda un metodo di un array:

    Array.prototype.slice.call(arguments, 1);

Ciò che è interessante notare è che l’oggetto ‘arguments’ non è in realtà per niente un array, tuttavia è spesso descritto come un oggetto di tipo array molto simile ad un ‘nodelist’ (cioè, qualsiasi cosa restituisca ‘document.getElementsByTagName()’). Gli ‘arguments’ di una funzione contengono una proprietà ‘length’ e valori indicizzati, ma non sono ‘array’, e per di più non supportano alcuni dei metodi nativi degli array come ‘slice’ e ‘push’. Tuttavia, a causa del loro comportamento simile, i metodi dell’oggetto Array possono essere adottati, ed eseguiti nel contesto di un oggetto array come come è il caso di cui sopra.

Questa tecnica di adottare metodi di un altro oggetto vale anche per la programmazione orientata agli oggetti quando si vuole emulare la ‘classica’ eredità (cioè legata al concetto di Classe), in JavaScript:

MyClass.prototype.init = function(){
    // call the superclass init method in the context of the "MyClass" instance
    MySuperClass.prototype.init.apply(this, arguments);
}

Richiamando il metodo della superclasse (‘MySuperClass’) nel contesto di un’istanza di una sottoclasse (‘MyClass’), possiamo imitare questo potente modello di progettazione.

In conclusione…

È importante capire questi concetti prima di iniziare ad avvicinarsi a modelli di progettazione avanzati, come cioè i concetti di ‘scope’ (ricordiamo, l’ambito di validità di una variabile) e il ‘context’ (cioè, il contesto di esecuzione di una funziona, il valore del ‘this’), svolgano un ruolo significativo e fondamentale nella moderna programmazione JavaScript.

Sia che si parli di ‘closure’, di orientamento agli oggetti ed ereditarietà, o delle varie implementazioni native, il ‘contesto’ e la ‘portata’ svolgono sempre un ruolo significativo. Se il vostro obiettivo è quello di padroneggiare il linguaggio Javascript e comprendere meglio tutto ciò che va oltre, lo scope e il contexr devono essere uno dei vostri punti di partenza!! :-)