What is this?
Comprendere l’ambito di this in JavaScript
Uno dei meccanismi che possono creare confusione quando si ha a che fare con JavaScript è la parola chiave this: è una parola chiave speciale che identifica un automatismo che definisce l’ambito di una funzione. Ogni funzione, mentre è in esecuzione, possiede un riferimento al contesto di esecuzione corrente, chiamato this, il quale non fa mai riferimento ad un tipo primitivo ma ad un oggetto. Ci sono poche regole fondamentali su come la parola chiave this viene associata al contesto di esecuzione e tutte dipendono da dove il codice di una funzione viene eseguito. Nel corso di questo articolo analizzeremo queste regole, partendo da alcuni concetti chiave del linguaggio JavaScript molto legate alla parola chiave this, come l’ambito ed il contesto.
Non facciamo confusione
Una delle cose fondamentali da non fare quando si parla della parola chiave this in JavaScript, è quella di pensare che si riferisca ad una classe, ad oggetti, istanze e tutte le altre cose del mondo della programmazione orientata agli oggetti. Inoltre, non bisogna paragonare questa parola chiave a quella degli altri linguaggi di programmazione. Un’altra cosa importante da chiarire è che il contesto (“Context”) e l’ambito (“Scope”) di una funzione non sono la stessa cosa. Molti sviluppatori confondono i due termini, descrivendo in modo errato uno per l’altro. Quindi chiariamo subito il significato di questi due concetti prima di proseguire.
Ambito e Contesto
L’ambito ha a che fare con la visibilità delle variabili. In JavaScript, si ottiene attraverso l’utilizzo delle funzioni. Quando usiamo la parola chiave var all’interno di una funzione, la variabile che stai inizializzando è privata per la funzione e non può essere vista al di fuori. Ma se dichiariamo altre funzioni al suo interno, allora quelle funzioni “interne” possono accedere a quella variabile perché si trovano nello stesso ambito. Le funzioni possono accedere anche alle variabili dichiarate al loro interno ed a quelle dichiarate all’esterno, ma mai a quelle dichiarate all’interno di funzioni nidificate. Questo in JavaScript prende il nome di ambito.
Il contesto, invece, è legato agli oggetti. Si riferisce all’oggetto a cui appartiene una funzione. Quando si utilizza la parola chiave JavaScript this, si fa riferimento all’oggetto a cui appartiene la funzione. Il contesto di esecuzione, in cui this viene valutato, altro non è che il luogo in cui una particolare funzione viene invocata e in che modo viene invocata.
Ambito globale
Per comprendere bene questa regola fondamentale, partiamo dal seguente frammento di codice:
function print () {
console.log(`My name is ${this.name}`)
}print()
Nelle prime tre linee di codice abbiamo dichiarato una funzione print che fa riferimento alla parola chiave this ed in particolare alla proprietà name. Subito dopo la dichiarazione di questa funzione la invochiamo. In questo caso this, non fa riferimento a nessun oggetto ma all’ambito globale di esecuzione del browser (e quindi Window), infatti provando a modificare il nostro console.log
nel seguente modo:
function print () {
console.log(this)
}print()
Otterremo il seguente risultato
Window {window: Window, self: Window, document: document, name: "", location: Location, …}
Quindi visto che l’abito globale non possiede alcuna proprietà name, non c’è da stupirsi se il risultato che verrà restituito è:
My name is
Questo accade quando il codice non viene eseguito in “modalità rigorosa”. Infatti se proviamo a modificare il codice aggiungendo 'strict mode'
all’inizio del nostro file:
'use strict'function print () {
console.log(`My name is ${this.name}`)
}print()
Otterremo un TypeError:
Uncaught TypeError: Cannot read property 'name' of undefined
at print (<anonymous>:4:34)
at <anonymous>:7:1
La modalità rigorosa di JavaScript, strict mode
, è un modo per optare per una variante limitata di JavaScript, disattivando così implicitamente la cosìddetta “modalità sciatta”. La modalità rigorosa non è solo un sottoinsieme: ha intenzionalmente una semantica diversa dal codice normale.
Binding implicito
Per comprendere la seconda regola riprendiamo la funzione print definita nell’esempio precedente e creiamo due oggetti con un riferimento ad essa. Ricordiamo che l’obiettivo è quello di essere in grado di guardare la definizione di una funzione utilizzando la parola chiave this e capire a cosa fa riferimento. Questa regola riuscirà a farci capire a cosa fa riferimento this nell’80% delle volte.
'use strict'function print () {
console.log(`My name is ${this.name}`)
}const person1 = { name: 'Paul', print: print }
const person2 = { name: 'Steve', print: print }
Nel codice appena mostrato abbiamo due oggetti, person1 e person2, con due proprietà. La prima proprietà è name: una stringa, che identifica il nome di una persona. Ora se vogliamo invocare la funzione print sull’oggetto person1 e person2 dobbiamo utilizzare il “punto” .
, nel modo seguente:
person1.print()
person2.print()
Questo ci porta al punto chiave principale della regola. Per capire a cosa fa riferimento la parola chiave this, dobbiamo guardare prima a sinistra del punto in cui la funzione viene invocata. Se è presente un “punto”, guarda a sinistra di quel punto per trovare l’oggetto a cui fa riferimento la parola chiave this.
Nell’esempio sopra, person1 e person2 si trovano a “sinistra del punto”, il che significa che la parola chiave this fa riferimento prima all’oggetto person1 e poi all’oggetto person2. Quindi è come se, all’interno della funzione print, l’interprete JavaScript cambiasse this in person1 e person2.
Eseguendo il codice, il risultato che otterrete sarà il seguente:
Paul
Steve
Ora facciamo un esempio simile, ma leggermente più avanzato: definiamo una terza proprietà, father, al nostro oggetto person2 che è il riferimento a person1:
'use strict'function print () {
console.log(`My name is ${this.name}`)
}const person1 = { name: 'Paul', print: print }
const person2 = { name: 'Steve', print: print, father: person1 }person2.print()
person2.father.print()
Come già detto in precedenza, l’80% delle volte dobbiamo vedere quello che c’è alla “sinistra del punto” per capire a chi fa riferimento this quando invochiamo la funzione print. Quando print viene invocata la prima volta l’oggetto alla sinistra del punto è person2 di conseguenza this.name farà riferimento alla stringa “Steve”. Quando print viene invocata la seconda volta, invece, alla sinistra del punto c’è la proprietà father, che è un riferimento a person1, di conseguenza this.name farà riferimento alla stringa “Paul”.
Binding Esplicito
Riconsideriamo per un attimo l’esempio fatto nel precedente paragrafo, ma questa volta senza creare alcun riferimento tra la funzione print e l’oggetto person:
'use strict'function print () {
console.log(`My name is ${this.name}`)
}const person = { name: 'Paul' }
Sappiamo che per sapere a cosa fa riferimento la parola chiave this, dobbiamo prima guardare dove viene invocata la funzione. Ora, questo solleva la domanda, come possiamo invocare print ma farlo invocare con la parola chiave this che fa riferimento all’oggetto utente? Non possiamo semplicemente utilizzare person.print() come in precedenza perché person non ha un metodo print. In JavaScript, ogni funzione contiene un metodo che ci consente di fare esattamente questo. E quel metodo si chiama call.
call
è un metodo presente su ogni funzione che permette di specificare, come primo parametro, il contesto in cui verrà invocata quella funzione. In altre parole, il primo argomento che passerai sarà ciò a cui fa riferimento la parola chiave this all’interno di quella funzione. Per maggiori informazioni consultare la documentazione.
Detto ciò, possiamo invocare print nel contesto person in questo modo:
print.call(person)
Rispetto alla regola di associazione implicita, questa volta non dobbiamo guardare alla “sinistra del punto”, bensì dobbiamo guardare solo ed esclusivamente al primo parametro della funzione call; quello sarà l’ambito a cui farà riferimento this nella funzione print. È esattamente questo il motivo per cui questa regola viene definita come binding esplicito, perché stiamo esplicitamente (utilizzando .call), specificando a cosa fa riferimento la parola chiave this.
'use strict'function print () {
console.log(`My name is ${this.name}`)
}const person = { name: 'Paul' }
print.call(person)
Eseguendo questo script il risultato sarà:
Paul
Ora proviamo a modificare la nostra funzione `print` in modo che abbia due ulteriori argomenti, gender ed age.
function print (gender, age) {
console.log(`My name is ${this.name}, I'm a ${gender} of ${age} years old!`)
}
Ora, se vogliamo passare gli argomenti alla funzione call, dobbiamo farlo uno alla volta dopo aver specificato il primo argomento che ne identifica il contesto:
'use strict'function print (gender, age) {
console.log(`My name is ${this.name}, I'm a ${gender} of ${age} years old!`)
}const person = { name: 'Paul' }
const arguments = ['male', 36]
print.call(person, arguments[0], arguments[1])
Ecco come passare argomenti a una funzione invocata con call. Tuttavia, è un po’ fastidioso dover passare gli argomenti uno per uno dal nostro array di argomenti. Sarebbe bello se potessimo passare l’intero array come secondo argomento e JavaScript li diffondesse per noi. Questo è esattamente ciò che fa apply.
apply funziona esattamente come .call, ma invece di passare gli argomenti uno per uno, possiamo passare un singolo array e apply si occuperà di distribuire ogni elemento dell’array per noi come argomenti della funzione.
Quindi ora, usando apply, il nostro codice può cambiare in questo modo:
'use strict'function print (gender, age) {
console.log(`My name is ${this.name}, I'm a ${gender} of ${age} years old!`)
}const person = { name: 'Paul' }
const arguments = ['male', 36]print.apply(person, arguments)
Finora, con la nostra regola “Legame esplicito”, abbiamo imparato a conoscere .call e .apply che consentono entrambi di invocare una funzione, specificando a cosa farà riferimento la parola chiave this all’interno di quella funzione. L’ultima funzione che dobbiamo conoscere ora è bind. Questa funzione è identica a call ma invece di invocare immediatamente la funzione, ne restituirà una nuova che possiamo invocare in un secondo momento. Quindi, se guardiamo il nostro codice precedente, utilizzando bind, sarà il seguente:
'use strict'function print (gender, age) {
console.log(`My name is ${this.name}, I'm a ${gender} of ${age} years old!`)
}const person = { name: 'Paul' }
const arguments = ['male', 36]const newPrint = print.bind(person, arguments[0], arguments[1])
newPrint()
L’operatore new
Un ulteriore modo per comprendere a cosa fa riferimento this è la parola chiave new. Se non hai familiarità con la questa parola chiave in JavaScript, considera che ogni volta che invochi una funzione con la parola chiave new, l’interprete JavaScript creerà un nuovo oggetto per te e lo chiamerà this. Quindi, naturalmente, se una funzione è stata chiamata con new, la parola chiave this fa riferimento a quel nuovo oggetto creato dall’interprete:
function Person (name, age) {
this.name = name
this.age = age
this.print = function () {
console.log(`Hello I'm ${this.name} and I'm ${this.age}!`)
}
}const me = new Person('Davide', 36)
me.print()
Lo stesso vale se utilizziamo le classi JavaScript:
class Person {
constructor(name, age) {
this.name = name
this.age = age
} print () {
console.log(`Hello I'm ${this.name} and I'm ${this.age}!`)
}
}const me = new Person('Davide', 36)
me.print()
In entrambi i casi, this farà riferimento all’oggetto creato con l’operatore new e in questo caso basta guardare “alla sinistra del punto” per capire a chi fa riferimento this. Negli esempi appena mostrati, il risultato che otterremo sarà il seguente:
Hello I'm Davide and I'm 36
Binding Lessicale
La parola chiave `this` in JavaScript è probabilmente più complessa di quanto dovrebbe essere. Ecco la buona notizia, questa prossima regola è la più intuitiva. È probabile che tu abbia sentito parlare e abbia usato le funzioni freccia (meglio conosciute come “arrow function”). Sono state introdotte a partire da ES6. Consentono di scrivere funzioni in un formato più conciso:
persons.map(person => person.name)
Ancor più della concisione, questo tipo di funzioni ha un approccio molto più intuitivo quando si tratta della parola chiave this. A differenza delle normali funzioni, le “arrow function” non hanno this. Invece, this è determinato lessicalmente. È un modo elegante per dire che this è determinato seguendo le normali regole di ricerca delle variabili nella catena prototipale. Chiariamo meglio il concetto con un esempio, prendendo in considerazione quanto segue:
const person = {
name: 'Davide',
age: 36,
gender: 'male',
frameworks: ['Fastify', 'React', 'Vue'],
print() {
const message = `My name is ${this.name},
I'm a ${this.gender} of ${this.age} years old!
My favourite frameworks are: `
const frms = this.frameworks.reduce(function (str, f, i) {
if (i === this.frameworks.length - 1) {
return `${str} and ${f}.`
} return `${str} ${f},`
}, "") console.log(message.concat(frms))
}
}person.print()
Se proviamo ad eseguire questo codice, avremo un errore:
if (i === this.frameworks.length - 1) {
^
TypeError: Cannot read property 'frameworks' of undefined
Quando invochiamo person.print(), ci aspettiamo di vedere “My name is Davide, I’m a male of 36 years old! …”. Secondo l’errore, this.frameworks non è definito. Esaminiamo i nostri passaggi per capire a cosa si riferisce this e soprattutto cerchiamo di capire perché non fa riferimento a person come dovrebbe essere.
Per prima cosa, dobbiamo guardare dove viene invocata la funzione. La funzione viene passata a .reduce quindi l’ambito di this non è lo stesso di person. In realtà non vediamo mai l’invocazione della nostra funzione anonima poiché JavaScript lo fa da solo nell’implementazione di .reduce. Questo è il problema. Dobbiamo specificare che vogliamo che la funzione anonima, che passiamo a .reduce, venga invocata nell’ambito di person. In questo modo this.frameworks farà riferimento a person.frameworks. Come abbiamo visto in precedenza, possiamo usare .bind:
const person = {
name: 'Davide',
age: 36,
gender: 'male',
frameworks: ['Fastify', 'React', 'Vue'],
print() {
const message = `My name is ${this.name},
I'm a ${this.gender} of ${this.age} years old!
My favourite frameworks are: `
const frms = this.frameworks.reduce(function (str, f, i) {
if (i === this.frameworks.length - 1) {
return `${str} and ${f}.`
} return `${str} ${f},`
}.bind(this), "") console.log(message.concat(frms))
}
}person.print()
Eseguendo il codice il risultato che otterremo sarà il seguente:
My name is Davide, I'm a male of 36 years old! My favourite frameworks are: Fastify, React, and Vue.
Abbiamo visto come .bind può risolverci ancora una volta il problema, ma in che modo è collegato alle arrow functions? Prima abbiamo detto che all’interno delle arrow functions, this viene determinato lessicalmente e quindi segue la catena prototipale finché non trova la variabile frameworks. Nell’esempio mostrato in precedenza, seguendo la catena prototipale, this fa riferimento proprio a person e quindi this.frameworks. Non c’è motivo di creare un nuovo contesto di esecuzione solo perché abbiamo utilizzato la funzione .reduce. Quindi rimuovendo la funzione .bind(this) ed utilizzando una arrow function anonima, il codice avrà il seguente aspetto:
const person = {
name: 'Davide',
age: 36,
gender: 'male',
frameworks: ['Fastify', 'React', 'Vue'],
print() {
const message = `My name is ${this.name},
I'm a ${this.gender} of ${this.age} years old!
My favourite frameworks are: `
const frms = this.frameworks.reduce((str, f, i) => {
if (i === this.frameworks.length - 1) {
return `${str} and ${f}.`
} return `${str} ${f},`
}, "") console.log(message.concat(frms))
}
}person.print()
Anche in questo caso il risultato che otterremo sarà:
My name is Davide, I'm a male of 36 years old! My favourite frameworks are: Fastify, React, and Vue.
Questo risultato dimostra come le “arrow function” non hanno il loro proprio this. Invece l’interprete JavaScript cercherà nell’ambito (genitore) che lo racchiude per determinare a cosa si riferisce.
Per concludere
In questo articolo abbiamo dato uno sguardo da vicino a this e a come il suo ambito cambia a seconda di come e dove viene utilizzato. Quindi, mettendo in pratica queste piccole regole, ogni volta che troverete la parola chiave this all’interno di una funzione riuscirete a capire a cosa si riferisce.
HAPPY, CODING!