Słowo kluczowe this w JavaScript - cztery reguły bindingu w jednym miejscu

this w JS zachowuje się dziwnie tylko dlatego, że ludzie patrzą nie tam, gdzie trzeba. Wartość this ustala się dopiero w momencie wywołania funkcji - są tylko cztery reguły, które to wywołanie może wybrać, plus arrow functions, które wyłamują się z całego systemu.
Klasyczna pułapka z this w JavaScripcie:
const user = {
name: "Adam",
greet() {
console.log(`Cześć, ${this.name}`);
},
};
user.greet(); // → "Cześć, Adam"
const fn = user.greet;
fn(); // → "Cześć, undefined"Ta sama funkcja, ten sam kod w środku, dwa różne wyniki. this raz pokazuje na obiekt user, raz na coś innego. Co tu nie pasuje?
Klucz: this nie jest własnością funkcji - jest własnością wywołania. Nie dowiesz się, na co this wskazuje, patrząc na funkcję. Dowiesz się patrząc na to, jak funkcja została wywołana.
W JavaScripcie są dokładnie cztery reguły wywołania, które ustalają wartość this. Plus arrow functions, które wyłamują się z całego systemu. Ten post jest o tym, jak rozróżnić, która reguła się odpala.
#this ustala się przy wywołaniu, nie przy definicji
Kiedy piszesz funkcję, nie zapisujesz w niej żadnej decyzji o this. To pole jest puste, czeka na uzupełnienie. Decyzja zapada dopiero w momencie, w którym ktoś tę funkcję woła i zapada w oparciu o to, jak ten ktoś ją zawoła.
Stąd przykład powyżej: user.greet() to wywołanie z udziałem obiektu (user), więc this zostaje powiązany z tym obiektem. fn() to gołe wywołanie - obiekt zniknął z miejsca wywołania, więc this zostaje powiązany z czymś innym.
Wniosek operacyjny: żeby się dowiedzieć, na co wskazuje this, patrz na miejsce wywołania, nie na definicję funkcji.
#Cztery reguły wywołania
Cztery wzorce wywołania, które silnik rozpoznaje, plus piąty przypadek (arrow function), który nie pasuje do żadnego z nich. Kliknij między zakładkami - każdy przykład pokazuje, jak różne wywołanie daje różną wartość this:
const user = {
name: "Adam",
greet() {
console.log(this.name);
}
};
user.greet();user"Adam"Obiekt user stoi tuż przed kropką w miejscu wywołania, więc this zostaje powiązany z tym obiektem.
Krótko o każdej regule:
- Default binding - gołe
fn()bez kropki przed.thisląduje naglobalThis(w trybie non-strict) lubundefined(w strict mode i w modułach ESM, które są strict domyślnie). - Implicit binding -
obj.method(). Liczy się to, co stoi tuż przed kropką. Pułapka:const fn = obj.method; fn()traci binding (wraca do default). - Explicit binding -
.call(thisArg),.apply(thisArg),.bind(thisArg). Jawne ustawieniethis..bindto „hard binding” - tworzy nową funkcję, którejthisnie da się już zmienić nawet przez.call. newbinding -new Foo(). Silnik tworzy nowy obiekt i ustawia go jakothis. Pełny mechanizm (linkowanie z prototype’em, return value handling) opisany jest w poście onew.
#Priorytety reguł
Co jeśli kilka reguł teoretycznie pasuje? Hierarchia, od najwyższego priorytetu:
newwygrywa ze wszystkim (newprzebija nawet.bind)- Explicit (
.call/.apply/.bind) wygrywa z implicit - Implicit (
obj.method()) wygrywa z default - Default to fallback
Praktycznie: patrz na wywołanie, identyfikuj najwyższy priorytet, który widzisz - to twoja reguła.
Spec ECMA-262 opisuje to formalnie w §10.2.1.1 OrdinaryCallBindThis - abstract operation odpalana przy każdym wywołaniu zwykłej funkcji, która wybiera odpowiednie powiązanie zgodnie z wewnętrznym slotem [[ThisMode]].
#Arrow functions: wyjątek od systemu
Arrow functions WYŁAMUJĄ się z czterech reguł. Nie mają własnego this. Gdy wewnątrz arrow function piszesz this, JavaScript traktuje to jak dowolną inną zmienną - szuka jej w lexical scope, czyli tam, gdzie arrow function została zdefiniowana.
Przykład w widget’cie powyżej (zakładka () => this) pokazuje to dokładnie: arrow w setTimeout nie ma własnego this, więc szuka w nadrzędnym scope. A nadrzędny scope to metoda greet(), w której this to user (implicit binding). Stąd arrow „widzi” user jako swoje this.
Mental shortcut: arrow function jest „transparentna” dla this - przepuszcza this z otoczenia. Klasyczna funkcja jest „nieprzezroczysta” - dostaje swój własny this zgodnie z miejscem wywołania.
#Puenta
Cały kanon this sprowadza się do dwóch rzeczy:
thisustala się przy wywołaniu, nie przy definicji. Patrz na to, jak funkcja została wywołana - to wszystko, czego potrzebujesz.- Hierarchia reguł:
new> explicit > implicit > default. Arrow functions wyłamują się z tej hierarchii (nie mają własnegothis, czerpią leksykalnie z otoczenia).
Jeśli widzisz w kodzie gdzieś this i nie wiesz, na co wskazuje: znajdź wywołanie, dopasuj regułę z hierarchii, masz odpowiedź.
Komentarze
Wczytywanie komentarzy…