L'interpretazione del codice JavaScript da parte di un engine
avviene in uno specifico contesto di esecuzione, cioè in un ambiente in cui vengono risolti i riferimenti alle variabili. Un contesto di esecuzione è composto dall'insieme delle variabili accessibili in un dato momento dell'esecuzione di un'istruzione, compresi gli eventuali argomenti di una funzione e l'oggetto this
.
Si tratta di un concetto estremamente importante in JavaScript ed è opportuno conoscerlo bene per comprendere alcuni meccanismi comunemente utilizzati ed evitare comportamenti indesiderati.
Un classico esempio è la corretta risoluzione dell'oggetto this, che rappresenta l'oggetto a cui è associata una proprietà o un metodo.
Consideriamo la definizione del seguente oggetto:
var persona = {
nome: "Mario",
cognome: "Rossi",
nomeCognome: function () {
return this.nome + " " + this.cognome;
}
};
Il metodo nomeCognome()
restituisce semplicemente la concatenazione delle due proprietà nome
e cognome
sfruttando l'oggetto this
.
Creiamo ora una funzione che prende per parametro il riferimento ad un altra funzione e tenta di lanciarla
// saluta() prende come parametro una funzione (nomepersona)
// e la esegue nel proprio contesto di esecuzione.
function saluta(nomePersona) {
console.log("Buongiorno " + nomePersona());
}
Facciamo in questo modo per invocare il metodo nomeCognome
in un diverso contesto di esecuzione, dove this
cambia di significato. Quindi invochiamo la funzione saluta
nel seguente modo:
// usiamo come parametro il metodo persona.nomeCognome
saluta(persona.nomeCognome);
In altre parole chiamando saluta(persona.nomeCognome)
non invochiamo il metodo nomeCognome()
ma lo passiamo alla funzione saluta()
, all'interno della quale verrà poi invocato (console.log("Buongiorno " + nomePersona())
).
Dal momento che nomeCognome()
è invocato al di fuori del suo contesto di esecuzione, this
non rappresenta più l'oggetto persona ma l'oggetto globale, nel quale non è presente alcuna definizione di variabili nome
e cognome
.
Il risultato che avremo è quindi:
Buongiorno undefined undefined
invece che "Buongiorno Mario Rossi".
Il problema nasce dal fatto che la funzione di callback nomePersona()
viene eseguita nel contesto di esecuzione della funzione saluta()
. In questo contesto di esecuzione, l'oggetto this
non rappresenta l'oggetto persona
ma, nel caso specifico, indica l'oggetto globale, che ad esempio in un browser corrisponde alla finestra corrente (window
).
Come facciamo quindi ad ottenere il nome e cognome dell'oggetto persona
?
Call e Apply
Avevamo detto che una funzione è un oggetto con proprietà e metodi. Per risolvere il nostro problema possiamo fare ricorso a due metodi dell'oggetto funzione che ci consentono di specificare il significato che intendiamo associare alla parola chiave this
.
Il primo metodo è call() che permette di invocare una funzione impostando il primo parametro come oggetto di riferimento per this
ed i parametri successivi, in numero variabile, come valori da passare alla funzione.
fun.call(thisArg[, arg1[, arg2[, ...]]])
Il secondo metodo è apply(), del tutto simile a call()
con la differenza che prevede due soli parametri: il primo è l'oggetto da associare a this
mentre il secondo parametro è un array dei valori da passare alla funzione da invocare.
fun.apply(thisArg, [argsArray])
Nel nostro caso possiamo quindi riscrivere la funzione saluta()
in questo modo:
function saluta(nomePersona)
{
console.log("Buongiorno " + nomePersona.call(persona));
}
Bind
Un approccio alternativo all'utilizzo di call()
e apply()
per impostare correttamente l'oggetto this
è l'uso del metodo bind(), introdotto nella versione 5 di ECMAScript.
A differenza di call()
e apply()
che impostano l'oggetto this
e gli eventuali parametri al momento della chiamata della funzione, bind()
consente di creare una nuova funzione con l'oggetto this
preimpostato. La nuova funzione creata da bind()
può essere invocata quindi in un secondo momento. Vediamo come sfruttare bind()
nel nostro caso:
function saluta(nomePersona) {
console.log("Buongiorno " + nomePersona());
}
saluta(persona.nomeCognome.bind(persona));
In questo caso, invece di intervenire sul codice della funzione saluta()
, abbiamo passato ad essa come parametro non direttamente il metodo nomeCognome()
ma una sua versione con this
preimpostato su persona
.
Segnaliamo che il metodo bind()
consente di specificare oltre al primo parametro una serie di parametri aggiuntivi che verranno automaticamente passati alla funzione al momento dell'invocazione.
Contesto di esecuzione in chiave DOM
Anche se non abbiamo ancora parlato delle API dei browser e del DOM, facciamo un esempio che spesso risulta utile nella pratica di utilità pratica. Riprendiamo un oggetto persona fatto in questo modo:
var persona = {
nome: "Mario",
cognome: "Rossi",
saluta: function() {
alert("Buongiorno " + this.nome + " " + this.cognome);
}
};
e associamo all'evento "click" su un bottone il metodo saluta
:
document.getElementById("pulsante").addEventListener("click", persona.saluta);
La situazione è analoga quella vista in precedenza: poiché quando viene invocato il metodo persona.saluta
il contesto di esecuzione sarà quello del singolo bottone, l'oggetto this
sarà diverso e otterremo il messaggio "Buongiorno undefined undefined
" invece di quello che ci aspetteremmo.