Przejdź do treści

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

Profile photo of Adrian Zawadzki

Adrian Zawadzki

6 min czytania

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();
Implicit binding
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:

  1. Default binding - gołe fn() bez kropki przed. this ląduje na globalThis (w trybie non-strict) lub undefined (w strict mode i w modułach ESM, które są strict domyślnie).
  2. Implicit binding - obj.method(). Liczy się to, co stoi tuż przed kropką. Pułapka: const fn = obj.method; fn() traci binding (wraca do default).
  3. Explicit binding - .call(thisArg), .apply(thisArg), .bind(thisArg). Jawne ustawienie this. .bind to „hard binding” - tworzy nową funkcję, której this nie da się już zmienić nawet przez .call.
  4. new binding - new Foo(). Silnik tworzy nowy obiekt i ustawia go jako this. Pełny mechanizm (linkowanie z prototype’em, return value handling) opisany jest w poście o new.

#Priorytety reguł

Co jeśli kilka reguł teoretycznie pasuje? Hierarchia, od najwyższego priorytetu:

  1. new wygrywa ze wszystkim (new przebija nawet .bind)
  2. Explicit (.call / .apply / .bind) wygrywa z implicit
  3. Implicit (obj.method()) wygrywa z default
  4. 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:

  1. this ustala się przy wywołaniu, nie przy definicji. Patrz na to, jak funkcja została wywołana - to wszystko, czego potrzebujesz.
  2. Hierarchia reguł: new > explicit > implicit > default. Arrow functions wyłamują się z tej hierarchii (nie mają własnego this, 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…

Dodaj komentarz