giovedì 20 marzo 2008

Le closure in Javascript

(Il seguente articolo e' estratto dal tutorial Javascript avanzato in corso di pubblicazione su www.ajaxcity.it, come parte III degli aspetti avanzati delle funzioni)

IL CONTESTO DI ESECUZIONE

Il contesto di esecuzione e' un concetto astratto usato nel documento di specifica del linguaggio ECMAScript (la versione standardizzata di javascript).
Non viene detto nulla in questo documento di specifica su come debba essere implementato questo concetto nella pratica.

Tutto il codice javascript viene eseguito in un contesto di esecuzione. Il codice globale, ovvero quello eseguito inline, normalmente come file JS o come pagina HTML, viene eseguito in un contesto di esecuzione globale, ed ogni chiamata a funzione avra' il proprio contesto di esecuzione associato.

Il codice eseguito con la funzione eval avra' un contesto di esecuzione distinto, ma poiche' eval di solito non viene utilizzato normalmente, se non nella formattazione JSON, per ora non viene trattata.

Il contesto di esecuzione di una funzione

Quando una funzione javascript viene invocata [con l'uso di "()" dopo il reference al nome della funzione], si inizializza un nuovo contesto di esecuzione; se viene chiamata una nuova funzione, o la stessa ricorsivamente, si crea un nuovo contesto di esecuzione e il programma in esecuzione entra in questo contesto per tutta la durata della chiamata a funzione; ritornando poi al contesto di origine quando la funzione chiamata restituisce il controllo al chiamante.

In questo modo il codice javascript in esecuzione forma una pila di contesti di esecuzione.

Creazione di un contesto di esecuzione per una chiamata a funzione

Quando viene creato un nuovo contesto di esecuzione, avvengono una serie di azioni in un ordine definito.

Per prima cosa, nel contesto di esecuzione di una funzione, viene creato un pseudo-oggetto "Activation", che ha proprieta' accessibili con un nome proprio ma non e' un oggetto normale perche' non ha un prototipo e non puo' essere referenziato direttamente usando codice javascript.

Il passo successivo e' la creazione dell'oggetto arguments, gia' incontrato nel nostro tutorial, con tutte le proprieta' collegate viste in precedenza.
Una proprieta' dell'oggetto Activation viene create col nome arguments e si assegna ad essa un reference all'oggetto arguments.

Activation.arguments --> Function.arguments

Come passo successivo si ha l'assegnazione di uno scope, o ambito, al contesto di esecuzione. Lo scope, o ambito, o ambiente, consiste in una lista (o elenco concatenato - catena - visto che si parla spesso di scope chain) di oggetti.

Ogni oggetto Function, ovvero ogni funzione (che in ultima istanza e' un oggetto in javascript) ha una proprieta' interna [[scope]] che anch'essa consiste di una lista di oggetti.

Lo scope assegnato al contesto di esecuzione di una chiamata a funzione e' composto dalla lista referenziata dalla proprieta' [[scope]] dell'oggetto funzione corrispondente con l'oggetto Activation aggiunto in testa a questa lista concatenata.

Riassumento un po' si ha quindi l'oggetto Function, ovvero la funzione stessa, con la sua proprieta' interna [[scope]] che referenzia una lista concatenata, una struttura dati esterna alla funzione, con in testa l'oggetto Activation della funzione, ovvero con in testa un reference all'oggetto Action della nostra funzione.

Quindi viene inizializzata l'instanziamento delle variabili, ovvero ad ogni parametro formale presente per la funzione viene associata una proprieta' dell'oggetto globale Variable-Activation (nella specifica questo oggetto viene nominato come Variable ma in definitiva si tratta dell'oggetto Activation creato in precedenza).

Se gli argomenti della chiamata a funzione corrispondono a questi parametri allora i valori di questi argomenti vengono assegnati alle proprieta' dell'oggetto Variable, senno' gli viene assegnato un valore "undefined".

Per le funzioni interne (ovvero le funzioni dichiarate dentro la nostra funzione appena chiamata) si creano oggetti funzione assegnati alle proprieta' dell'oggetto Variable con nomi che corrispondono ai nomi usati nelle dichiarazioni delle funzioni interne.

L'ultimo passo e' quello di creare le proprieta' dell'oggetto Variable che corrispondono a tutte le variabili dichiarate all'interno della nostra funzione, mettendo inizialmente un valore "undefined" fino a quando non avviene la valutazione delle espressioni di assegnamento durante l'esecuzione vera e propria del codice della nostra funzione.

E' proprio il fatto che l'oggetto Activation, con la sua proprieta' arguments, e l'oggetto Variable con tutte le sue proprieta' che corrispondeono alle variabili locali della funzione, siano lo stesso oggetto, che permette all'identificatore arguments di essere usato come una variabile locale, ovvero richiamandolo come [Function.]arguments all'interno del corpo della funzione.

Infine, si assegna un valore da poter utilizzare con la keyword this.
In pratica viene assegnato o l'oggetto proprietario della funzione, oppure l'oggetto globale se viene assegnato (internamente) un valore 'null'.

Il contesto di esecuzione globale invece ha alcune leggeri differenze poiche' non possiede argomenti cosicche' non necessita di un oggetto Activation definito che lo referenzi.

Il contesto globale non ha bisogno di uno scope, e la sua scope chain, cioe' la lista associata, consiste solo di un oggetto, l'oggetto globale primitivo.
Questo oggetto globale primitivo viene usato come oggetto Variable, cosa per cui le funzioni dichiarate globalmente diventano proprieta' dell'oggetto globale, come fossero variabili dichiarate globalmente.

Il contesto globale utilizza anche un reference all'oggetto globale per l'oggetto this.

Si vede quindi che tutto si riduce ad un oggetto, globale o no, con una propria lista (o scope chain) ed un oggetto Activation/Variable che contiene nelle sue proprieta' tutti i componenti mappati dell'oggetto iniziale.

LA SCOPE CHAIN O LISTA DEGLI AMBITI DI VISIBILITA' DELLE VARIABILI

La lista degli ambiti di visibilita' delle variabili per una chiamata a funzione, o catena di scope, viene costruita aggiungendo l'oggetto Activation/Variable in testa alla lista tenuta nella proprieta' [[scope]] della nostra funzione.

Cerchiamo quindi di capire come viene definita questa proprieta' interna [[scope]].

Nella specifica standardizzata del linguaggio javascript (ECMAScript) le funzioni sono oggetti. Vengono creati o durante le istanziazioni delle variabili dalle dichiarazioni delle funzioni (quando si usa la parola chiave function), o durante il processo di valutazione delle espressioni funzionali (quando si usano le funzioni anonime var fn=function()) oppure invocando il construttore Function.

Gli oggetti creati usando il costruttore Function hanno sempre una proprieta' [[scope]] che referenzia una lista di scope che contiene solo l'oggetto globale "{}".

Se creiamo un oggetto funzione con gli altri due metodi avremo invece una lista di scope del contesto di esecuzione in cui viene creato assegnata alla loro proprieta' interna [[scope]].

Se il contesto di esecuzione e' quello globale, ovvero se definiamo normalmente una funzione in un file JS eseguito all'interno della nostra pagina web, anch'essa conterra' solo l'oggetto globale.

Se definiamo una funzione utilizzando una espressione funzionale del tipo

var myFunction = function(formalParameter){
//function body code
}


viene creata inizialmente una proprieta' con il nome della variabile myFunction, senza creare l'oggetto per la funzione. Siamo pero' sempre nel contesto di esecuzione globale.
E quando si valuta l'espressione si crea l'oggetto Function e viene passato alla proprieta' della variabile un riferimento a questo oggetto.
Comunque l'oggetto viene creato sempre nel contesto di esecuzione globale.

Le dichiarazioni e le espressioni di funzioni annidate (inner functions) dentro altre funzioni fanno si' che l'oggetto risultante venga creato dentro il contesto di esecuzione di una funzione, cosi' che si abbiano liste di scope piu' elaborate.

Consideriamo il codice seguente in cui definiamo una funzione con una dichiarazione di funzione annidata al suo interno, e che poi esegue la funzione esterna:

function funzioneEsterna(par){
function funzioneInterna(){
//corpo della funzione interna
}
//resto delle istruzioni della funzione esterna
}
funzioneEsterna(5);


L'oggetto corrispondente alla dichiarazione della funzione esterna {funzioneEsterna} viene creato durante il setup delle variabili nel contesto di esecuzione globale cosi' che la proprieta' [[scope]] contiene la lista di scope composta dal solo oggetto globale.

Quando il codice globale esegue la chiamata alla funzione esterna viene creato un nuovo contesto di esecuzione per quella chiamata a funzione insieme ad un oggetto Variable/Activation insieme ad esso.
L'ambito di visibilita' delle variabili di questo nuovo contesto di esecuzione diventa la catena composta dal nuovo oggetto Activation seguito dalla lista referenziata dalla proprieta' [[scope]] dell'oggetto {funzioneEsterna}, ovvero solo l'oggetto globale {}.

Il setup dell'oggetto Variable per questo nuovo contesto di esecuzione si riflette nella creazione di un oggetto funzione che corrisponde alla definizione della funzione interna e la proprieta' [[scope]] di questo oggetto funzione viene referenziata con lo scope del contesto di esecuzione in cui viene creato, ovvero la lista di scope della funzione esterna, cioe' una lista con l'oggetto Activation seguito dall'oggetto globale {}.

Riassumento quindi generalmente quando si definisce una funzione solitamente viene creato un oggetto con scope globale, cioe' con una lista di scope associata alla proprieta' [[scope]] di quell'oggetto che contiene l'oggetto globale come unico termine della lista.

Risoluzione degli identificatori

Tutto il complesso discorso appena evidenziato ci serve per capire il meccanismo di risoluzione degli identificatori che avviene in Javascript.

Gli identificatori vengono risolti contro (against) la scope chain, o lista degli ambiti di visibilita' delle variabili.

La risoluzione degli identificatori inizia con il primo oggetto nella lista degli scope.
Si verifica se questo primo oggetto possiede una proprieta' con nome uguale all'identificatore.

La lista di scope e' una lista di oggetti, e gli oggetti possiedono una propria lista di prototipi, come abbiamo visto in una lezione precedente, perche' un oggetto puo' derivare da un altro oggetto come suo prototipo, e cosi' via fino al prototipo dell'oggetto globale.

Per cui questo controllo delle proprieta' dell'oggetto contenuto nella lista degli scope significa ripercorrere tutta la catena dei prototipi di quell'oggetto (se ne possiede una). Se non viene trovato un valore corrispondente nelle proprieta' del primo oggetto si passa a controllare all'oggetto successivo nella lista degli scope, se ci sono altri oggetti in essa.

Tutto cio' si ripete fino a quando viene trovata una proprieta' di qualche oggetto nella lista degli scope con un nome che corrisponde all'identificatore o fino a quando si esaurisce la scope chain.

L'oggetto globale e' sempre alla fine della lista degli scope.

Poiche' i contesti di esecuzione associati con le chiamate a funzione avranno sempre l'oggetto Activation in cima alla lista degli scope, gli identificatori locali utilizzati nel corpo della funzione verrano controllati sempre per primi per vedere se corrispondono con i parametri formali della funzione, con i nomi delle funzioni interne o con le variabili locali. Costoro verranno risolti come proprieta' nominali dell'oggetto Activation/Variable.

LE CLOSURE IN JAVASCRIPT

Proviamo, alla luce del discorso appena concluso nel capitolo precedente, un esempio di codice come il seguente:

var myObject = function() {...} ();


Vediamo che abbiamo creato una variabile chiamata myObject e gli stiamo associando una funzione anonima che non possiede argimenti formali.

La novita' rispetto a quanto visto finora e' la presenza delle due parentesi tonde () appena dopo le parentesi graffe che nelle nostre intenzioni racchiudono il corpo della funzione.

Queste parentesi tonde finali indicano a javascript di eseguire la funzione immediatamente dopo la creazione dell'oggetto Function associato durante la fase di parsing della definizione della funzione stessa.

Cio' significa che javascript non associa piu' alla variabile myObject un reference all'oggetto Function, come visto prima, ma associera' il risultato della funzione, o piu' propriamente cio' che viene restituito dalla funzione al termine della sua esecuzione.

Se la nostra funzione fosse una semplice funzione con all'interno solo variabili locali ed espressioni di calcolo che utilizzano le variabili locali, il valore ritornato sara' presumibilmente un singolo valore che verra' quindi associato alla variabile myObject, facendo si' che il contesto di esecuzione si esaurisca con l'esecuzione della funzione e permettendo al meccanismo di garbage collection (GC) di javascript di riciclare il contenuto dell'oggetto Activation/Variable creato durante la chiamata a funzione e l'oggetto Function stesso visto che si tratta di una funzione anonima che non puo' piu' essere richiamata altrove.

Il meccanismo di Garbage Collection di Javascript

In javascript, come in Java, si ha un meccanismo automatico di Garbage Collection, ovvero di raccolta della spazzatura.

Per i linguaggi di programmazione la spazzatura e' tutto l'insieme di oggetti che una volta esaurita la loro esistenza una volta compiuto il compito per cui sono stati creati, e che quindi occupano memoria.

Una funzione anonima una volta eseguita non puo' piu' essere richiamata, non avendo un nome. Quindi l'oggetto Function, e l'oggetto Activation creato durante la sua esecuzione, devono essere smaltiti per liberare la memoria occupata.

Piu' generalmente un oggetto che non puo' piu' essere referenziato diventa disponibile per il suo smaltimento e distruzione.

Il meccanismo di Garbage Collection si occupa di ricercare periodicamente gli oggetti non piu' referenziabili da smaltire e della loro effettiva distruzione con conseguente liberazione della memoria occupata.

Creazione di una Closure

Se pero' la nostra funzione anonima possedesse una o piu' funzioni interne facenti parte dell'espressione di return della nostra funzione principale, alla variabile myObject verrebbe ora associato un oggetto contenente le funzioni interne restituite col return:

var myObject = function() {
var privateVar = 'Questa variabile è privata!';
var privateFunction = function() {
alert('Questa funzione è privata!');
}
return {
showPrivateVar : function () {
alert(privateVar);
},
modifyPrivateVar : function(val) {
privateVar = val;
},
executePrivateFunc : function() {
privateFunction();
}
}
}();


In questo caso la nostra funzione anonima "esterna" possiede localmente una variabile privateVar pura ed una variabile privateFunction a cui viene associata una funzione interna anonima - diremo quindi piu' semplicemente che possiede la funzione interna privateFunction tanto ora abbiamo capito il giro che viene fatto.

Inoltre nella sezione retituita all'esterno dalla sua esecuzione troviamo, nella notazione ad oggetti di javascript, tre ulteriori funzioni (chiamate comunemente "accessors" o modificatori) che operano sulla variabile e sulla funzione interna locali.

Se la funzione esterna ritornasse solo un valore come risultato, non potremmo piu' accedere a privateVar o a privateFunction una volta eseguita la funzione esterna.

Cioe' il contesto di esecuzione si esaurirebbe con la funzione, e l'oggetto Activation/Variable verrebbe dereferenziato completamente appartenendo allo scope interno alla funzione con lista composta dall'oggetto Activation e dall'oggetto globale {}.

Invece la variabile myObject ha uno scope globale con una lista composta solo dall'oggetto globale {}.

Invece quando vengono restituite delle funzioni interne all'esterno della funzione esterna, con l'istruzione return come nel caso riportato con il codice javascript, l'oggetto ritornato (che consta di tre funzioni) referenzia lo scope interno alla funzione esterna, perche' le tre funzioni interne hanno una scope chain composta giustamente dall'oggetto Activation/Variable che referenzia gli elementi locali del corpo della funzione esterna, e dall'oggetto globale {}.

Questo oggetto quindi viene associato alla variabile con scope globale myObject.

Ecco creata una closure (o "chiusura") dello scope interno alla funzione eseguita sullo scope globale della variabile myObject.

In questo modo, poiche' la variabile myObject puo' referenziare ancora l'oggetto Activation attraverso uno dei tre metodi "acquisiti", questo oggetto non puo' piu' essere eliminato dal meccanismo di GC della memoria.

In pratica la funzione esterna una volta eseguita non esiste piu'. Esiste solo il risultato della funzione. Pero' le sue variabili e funzioni private esistono ancora e possono essere accedute/modificate attraverso le funzioni di accesso/modifica acquisite dalla variabile globale myObject:

Quando ora chiamiamo
myObject.showPrivateVar()
si ottiene un alert box con il messaggio 'Questa variabile è privata!';

Allo stesso modo se chiamiamo
myObject.modifyPrivateVar('nuovo valore')
otteniamo la modifica del valore della variabile privata (ed e' l'unico modo con cui questa variabile privata puo' essere modificata, solo se viene fornito all'esterno un modificatore).
Se quindi adesso richiamiamo
myObject.executePrivateFunc()
otteniamo un alert box con il messaggio 'nuovo valore'.

Questo modo di creare una closure permette la creazione di namespace e l'incapsulamento dei metodi e delle proprieta' locali degli Oggetti in javascript.

myObject agisce come namespace, e le tre funzioni di accesso/modifica nascondono le variabili e i metodi privati della funzione esterna che ora puo' essere vista come Oggetto/Classe che esporta verso l'esterno tre metodi pubblici.

Ci sono molti altri modi per creare una closure.

In pratica ogni volta che viene restituita una funzione interna al di fuori di una funzione esterna si forma una chiusura dello scope interno sullo scope esterno. Se si ha quindi un reference associato ad un elemento globale, come nel nostro caso la variabile myObject, oppure quando si associa un eventhandler ad un evento di un elemento del Document Object Model della nostra pagina web, l'oggetto Activation dello scope locale sopravvive alla terminazione della funzione esterna.

Per approfondimenti ulteriori su altri modi di creare closure vi rimando all'articolo di Claudio Cicali (http://stacktrace.it/articoli/2007/12/javascript-closures/), oppure al 'must-read' di Richard Cornford (http://www.jibbering.com/faq/faq_notes/closures.html) da cui ho tratto spunto per questo capitolo sulle closure soprattutto sul discorso preliminare dell'execution context e sulla struttura della scope chain.

Nessun commento: