Determinare il valore di this
può essere complicato in JavaScript, ecco come fare…
Il this
di JavaScript è oggetto di molte barzellette, perché è piuttosto complicato.
Tuttavia, ho visto sviluppatori fare cose molto più complicate e specifiche del dominio per evitare di gestire questo problemathis
. Se hai dubbi in merito a this
, spero che queste informazioni ti siano di aiuto. Questa è la mia guida this
.
Inizierò con la situazione più specifica e terminerò con la meno specifica. Questo articolo è un po' come un grande if (…) … else if () … else if (…) …
, quindi puoi andare direttamente alla prima sezione che corrisponde al codice che stai esaminando.
- Se la funzione è definita come funzione freccia
- In caso contrario, se la funzione/la classe viene chiamata con
new
- In caso contrario, se la funzione ha un valore
this
"limitato" - In caso contrario, se
this
è impostato al momento della chiamata - In caso contrario, se la funzione viene chiamata tramite un oggetto principale (
parent.func()
) - In caso contrario, se la funzione o l'ambito principale è in modalità rigorosa
- In caso contrario
Se la funzione è definita come funzione freccia:
const arrowFunction = () => {
console.log(this);
};
In questo caso, il valore di this
è sempre uguale a this
nell'ambito principale:
const outerThis = this;
const arrowFunction = () => {
// Always logs `true`:
console.log(this === outerThis);
};
Le funzioni freccia sono ottime perché il valore interno di this
non può essere modificato, è sempre uguale al valore esterno di this
.
Altri esempi
Con le funzioni freccia, il valore di this
non può essere modificato con bind
:
// Logs `true` - bound `this` value is ignored:
arrowFunction.bind({foo: 'bar'})();
Con le funzioni freccia, il valore di this
non può essere modificato con call
o apply
:
// Logs `true` - called `this` value is ignored:
arrowFunction.call({foo: 'bar'});
// Logs `true` - applied `this` value is ignored:
arrowFunction.apply({foo: 'bar'});
Con le funzioni freccia, il valore di this
non può essere modificato chiamando la funzione come membro di un altro oggetto:
const obj = {arrowFunction};
// Logs `true` - parent object is ignored:
obj.arrowFunction();
Con le funzioni freccia, il valore di this
non può essere modificato chiamando la funzione come
costruttore:
// TypeError: arrowFunction is not a constructor
new arrowFunction();
Metodi di istanza "bound"
Con i metodi di istanza, se vuoi assicurarti che this
faccia sempre riferimento all'istanza di classe, il modo migliore è utilizzare le funzioni freccia e i campi di classe:
class Whatever {
someMethod = () => {
// Always the instance of Whatever:
console.log(this);
};
}
Questo pattern è molto utile quando si utilizzano metodi di istanza come ascoltatori di eventi nei componenti (ad esempio i componenti React o web).
Potrebbe sembrare che il codice riportato sopra violi la regola "this
sarà uguale a this
nell'ambito principale", ma inizia ad avere senso se pensi ai campi di classe come a un abbellimento sintattico per l'impostazione di elementi nel costruttore:
class Whatever {
someMethod = (() => {
const outerThis = this;
return () => {
// Always logs `true`:
console.log(this === outerThis);
};
})();
}
// …is roughly equivalent to:
class Whatever {
constructor() {
const outerThis = this;
this.someMethod = () => {
// Always logs `true`:
console.log(this === outerThis);
};
}
}
I pattern alternativi prevedono l'associazione di una funzione esistente nel costruttore o l'assegnazione della funzione nel costruttore. Se per qualche motivo non riesci a utilizzare i campi di classe, l'assegnazione di funzioni nel constructor è un'alternativa ragionevole:
class Whatever {
constructor() {
this.someMethod = () => {
// …
};
}
}
In caso contrario, se la funzione/la classe viene chiamata con new
:
new Whatever();
Il codice riportato sopra chiamerà Whatever
(o la relativa funzione di costruttore se si tratta di una classe) con this
impostato sul risultato di Object.create(Whatever.prototype)
.
class MyClass {
constructor() {
console.log(
this.constructor === Object.create(MyClass.prototype).constructor,
);
}
}
// Logs `true`:
new MyClass();
Lo stesso vale per i costruttori di tipo precedente:
function MyClass() {
console.log(
this.constructor === Object.create(MyClass.prototype).constructor,
);
}
// Logs `true`:
new MyClass();
Altri esempi
Se viene chiamato con new
, il valore di this
non può essere modificato con bind
:
const BoundMyClass = MyClass.bind({foo: 'bar'});
// Logs `true` - bound `this` value is ignored:
new BoundMyClass();
Se viene chiamato con new
, il valore di this
non può essere modificato chiamando la funzione come membro di un altro oggetto:
const obj = {MyClass};
// Logs `true` - parent object is ignored:
new obj.MyClass();
In caso contrario, se la funzione ha un valore this
"bound":
function someFunction() {
return this;
}
const boundObject = {hello: 'world'};
const boundFunction = someFunction.bind(boundObject);
Ogni volta che viene chiamato boundFunction
, il suo valore this
sarà l'oggetto passato a bind
(boundObject
).
// Logs `false`:
console.log(someFunction() === boundObject);
// Logs `true`:
console.log(boundFunction() === boundObject);
Altri esempi
Quando viene chiamata una funzione vincolata, il valore di this
non può essere modificato con call
o
apply
:
// Logs `true` - called `this` value is ignored:
console.log(boundFunction.call({foo: 'bar'}) === boundObject);
// Logs `true` - applied `this` value is ignored:
console.log(boundFunction.apply({foo: 'bar'}) === boundObject);
Quando chiami una funzione legata, il valore di this
non può essere modificato chiamando la funzione come membro di un altro oggetto:
const obj = {boundFunction};
// Logs `true` - parent object is ignored:
console.log(obj.boundFunction() === boundObject);
In caso contrario, se this
è impostato al momento della chiamata:
function someFunction() {
return this;
}
const someObject = {hello: 'world'};
// Logs `true`:
console.log(someFunction.call(someObject) === someObject);
// Logs `true`:
console.log(someFunction.apply(someObject) === someObject);
Il valore di this
è l'oggetto passato a call
/apply
.
Purtroppo this
viene impostato su un altro valore da elementi come gli ascoltatori di eventi DOM e il suo utilizzo può generare codice difficile da comprendere:
element.addEventListener('click', function (event) {
// Logs `element`, since the DOM spec sets `this` to
// the element the handler is attached to.
console.log(this);
});
Evito di utilizzare this
in casi come quello riportato sopra e, al suo posto:
element.addEventListener('click', (event) => {
// Ideally, grab it from a parent scope:
console.log(element);
// But if you can't do that, get it from the event object:
console.log(event.currentTarget);
});
In caso contrario, se la funzione viene chiamata tramite un oggetto principale (parent.func()
):
const obj = {
someMethod() {
return this;
},
};
// Logs `true`:
console.log(obj.someMethod() === obj);
In questo caso la funzione viene chiamata come membro di obj
, quindi this
sarà obj
. Questo accade al momento della chiamata, quindi il collegamento viene interrotto se la funzione viene chiamata senza l'oggetto principale o con un oggetto principale diverso:
const {someMethod} = obj;
// Logs `false`:
console.log(someMethod() === obj);
const anotherObj = {someMethod};
// Logs `false`:
console.log(anotherObj.someMethod() === obj);
// Logs `true`:
console.log(anotherObj.someMethod() === anotherObj);
someMethod() === obj
è falso perché someMethod
non viene chiamato come membro di obj
. Potresti
aver riscontrato questo problema quando hai provato qualcosa di simile:
const $ = document.querySelector;
// TypeError: Illegal invocation
const el = $('.some-element');
Il problema si verifica perché l'implementazione di querySelector
esamina il proprio valore this
e si aspetta che sia un tipo di nodo DOM, mentre il codice riportato sopra interrompe questa connessione. Per eseguire correttamente quanto sopra:
const $ = document.querySelector.bind(document);
// Or:
const $ = (...args) => document.querySelector(...args);
Curiosità: non tutte le API utilizzano this
internamente. I metodi della console come console.log
sono stati modificati per evitare riferimenti a this
, quindi log
non deve essere associato a console
.
In caso contrario, se lo scopo della funzione o del contesto principale è in modalità rigorosa:
function someFunction() {
'use strict';
return this;
}
// Logs `true`:
console.log(someFunction() === undefined);
In questo caso, il valore di this
non è definito. 'use strict'
non è necessario nella funzione se l'ambito principale è in modalità rigorosa (e tutti i moduli sono in modalità rigorosa).
Altrimenti:
function someFunction() {
return this;
}
// Logs `true`:
console.log(someFunction() === globalThis);
In questo caso, il valore di this
è uguale a globalThis
.
Finalmente.
È tutto. È tutto quello che so su this
. Domande? Mi è sfuggito qualcosa? Non esitare a twittarmi.
Grazie a Mathias Bynens, Ingvar Stepanyan e Thomas Steiner per la revisione.