Co to jest `this` w JavaScript #66

Jedną z najtrudniejszych rzeczy w JavaScript jest zrozumienie jak działa this. Mechanizm ten sprawia wiele problemów każdemu programiście. Nawet doskonale rozumiejąc działanie this możemy popełnić błąd. Spróbujmy krok po kroku przeanalizować czym jest this.

W kontekście this będziemy mówić o funkcjach i metodach. Funkcje rozumiem jako samodzielne byty w kodzie. Natomiast metody są to funkcje, które zdefiniowane są w obiekcie.

Moje przykłady pokazuję w przeglądarce, pracuję bez trybu ścisłego. Moim globalnym obiektem jest obiekt window. Przygotowane przykłady uruchamiam także bezpośrednio w przeglądarce, nie korzystam z żadnych bundlerów jak Parcel czy Webpack. Po prostu VanillaJs.

Mówię, to, ponieważ w przypadku używania bundlerów, frameworków lub środowiska node.js przykłady te mogą działać inaczej. Dodatkowo, jeżeli będziemy omawiać moduły ES6, będziemy musieli obecną wiedzę, trochę zweryfikować.

Co to jest kontekst wykonania

Bardzo często spotkacie się z definicją, że this jest kontekstem wykonania, dlatego na początku wyjaśnijmy sobie, czym jest kontekst wykonania. W JavaScript, kod możemy wykonać albo w jakieś funkcji czy metodzie, czyli w kontekście tej funkcji lub metody, albo w kontekście globalnym.

Jeżeli otworzymy sobie plik JavaScript i zaczniemy pisać kod, a potem wczytamy go do przeglądarki, będziemy mieli kontekst globalny:

this.foo = 42;
console.log('Hello World');
console.log(this.foo); // 42
console.log(foo); // 42
console.log(this); // object Window

Ten cały kod, który widzicie, został wykonany w kontekście globalnym. Kontekst zawsze możemy wypisać przez wskaźnik this i widzimy, że wypisując go otrzymuję obiekt window. Obiekt window jest globalnym kontekstem dla wykonania kodu w JavaScript i jest kontekstem domyślnym.

Stwórzmy teraz funkcję:

function printThis() {
  console.log(this);
}

printThis(); // object Window

Stworzyłem funkcję i wywołuję ją bezpośrednio w pliku JavaScript. Kontekstem wykonania jest obiekt globalny window. Widzimy to przez wypisanie this do konsoli w środku funkcji.

Zróbmy jednak pewien eksperyment:

const obj = {
  a: 'My object',
  print: printThis,
};

Stworzyłem dodatkowo obiekt, z polem a opisującym ten obiekt, oraz stworzyłem pole print, do którego przypisuję wcześniej stworzoną funkcję printThis. Funkcję tą przypisuję przez referencję, od tego momentu, gdy odwołam się przez obj.print to będę wywoływał tak naprawdę funkcję printThis,
która zdeklarowana jest poza moim obiektem.

Wywołajmy więc metodę print w moim obiekcie:

obj.print(); // {a: "My object", print: ƒ}

Okazuje się, że funkcja printThis nie wypisała do konsoli obiektu globalnego window, ale ten mój stworzony obiekt. Funkcja printThis pracuje teraz w zupełnie innym kontekście, w kontekście mojego stworzonego obiektu, a nie obiektu window.

To jest właśnie to, co najbardziej zaskakuje początkujących programistów JavaScript. Kontekst this dla funkcji czy metod może się zmieniać. To, czym jest this dla funkcji zależy tylko i wyłącznie od sposobu jej wykonania, nie od miejsca jej deklaracji. Mało tego, kontekst funkcji możemy zmienić
kilka razy w czasie wykonywania kodu.

Dochodzimy pomału do wniosku, że kontekstem wykonania dla funkcji czy metody jest jakiś obiekt. Domyślnie jest to obiekt globalny. W przeglądarce obiektem globalnym jest window gdy nie ma włączonego trybu ścisłego. Może to być też obiekt, który stoi przed kropką, gdy wywołujemy metodę. Miejsce
deklaracji funkcji czy metody nie ma żadnego znaczenia. Znaczenie ma tylko to, na jakim kontekście wywołana została funkcja lub metoda.

Jak łatwo określić this

Wiemy już, że this zależy od tego, jak została wywołana funkcja, a ściślej mówiąc, na jakim obiekcie została wywołana funkcja. Kontekst wykonywania funkcji może się więc zmienić i w ogóle nie zależy od tego, gdzie dana funkcja została stworzona:

const objectA = {
  a: 'objectA',
  bar() {
    console.log('Your this is:', this);
    console.log(this.a);
  },
};

Zobaczmy taki przykład, stworzyłem obiekt objectA, z polem a opisującym ten obiekt, oraz z metodą bar. Zadaniem tej metody jest wypisanie this do konsoli, a także wypisanie this.a do konsoli.

objectA.bar();
Your this is: {a: "objectA", bar: ƒ}
objectA

Gdy wywołam metodę objectA.bar() otrzymuję informację, że this to ten obiekt, w którym jest metoda. Jest to dla nas naturalne, że jeżeli metoda bar() znajduje się w obiekcie objectA to this zawsze odnosi się do tego obiektu. Jednak nie należy przyzwyczajać się do takiego myślenia w
JavaScript.

Tak jak wspomniałem, this zależy tylko i wyłącznie od sposobu wywołania metody, nie od miejsca deklaracji, zobaczcie taki przykład:

const objectB = {
  a: 'objectB',
  foo: objectA.bar,
};
objectB.foo();

Deklaruję obiekt objectB, który ma pole a z opisem obiektu oraz pole foo. Do pola foo przypisuję referencję do metody bar() pochodzącej z objectA. Wywołuję teraz metodę objectB.foo() co tak naprawdę wywołuje metodę bar() w obiekcie objectA:

Your this is: {a: "objectB", foo: ƒ}
objectB

Jednak metoda, która fizycznie znajduje się w obiekcie objectA wypisuje dane z objectB. To udowadnia, że nie ma znaczenia, gdzie istnieje metoda czy funkcja, tylko jaki jest kontekst wywołania. W tym przypadku kontekstem jest to co stoi przed kropką, gdy wywołujemy metodę, a jest to objectB.

Zawsze zawracajmy uwagę na obiekt, który znajduje się przed kropką wywołania metody. To ten obiekt będzie this w tej metodzie czy funkcji.

Stwórzmy jeszcze jeden przykład:

const baz = objectA.bar;
baz();

Stworzyłem teraz zmienną w kontekście globalnym i przypisuję do niej referencję do metody bar z obiektu objectA oraz wywołuję zmienną baz():

Your this is: Window{…}
undefined

W tym przypadku kontekstem wykonania jest obiekt globalny, a przeglądarce jest to window. Widzicie, że znowu zmieniłem kontekst wykonania metody. Co prawda nie mamy tutaj obiektu znajdującego się przed kropką wywołania funkcji baz(). Jeżeli taki obiekt nie istnieje, kontekstem domyślnym jest
obiekt globalny, a w przeglądarce jest to window. Wywołanie funkcji czy metody musi odbywać się zawsze w jakimś kontekście obiektu.

This a tryb ścisły

Swoje przykłady pokazuję bez trybu ścisłego, który został wprowadzony w ES5 i używamy go poprzez polecenie use strict
na początku pliku lub funkcji. Robię tak, aby wam pokazać, jak standardowo działa JavaScript. Jednak w większości przypadków będziecie pracować z JavaScript, używając frameworków i modułów ES6, gdzie tryb ścisły jest trybem domyślnym. Również klasy w JavaScript pracują w trybie ścisłym. Zobaczmy
teraz jaka jest różnica w odniesieniu do wskaźnika this.

'use strict';

function printThis() {
  console.log(this);
}

printThis(); // undefined

Stworzyłem funkcję w trybie ścisłym i wypisuję wewnątrz funkcji this. Funkcję wywołuję bez żadnego obiektu zdefiniowanego przeze mnie, więc domyślnie z obiektem globalnym. W trybie ścisłym wewnątrz funkcji nie mam dostępu do obiektu globalnego window i otrzymuję wartość undefined.

Nie oznacza to, że obiekt ten przestał istnieć, wciąż możemy się do niego odwołać przez window lub globalThis:

console.log(window) // object window
console.log(globalThis) // object window
console.log(this) // object window

Domyślnie w funkcji i trybie ścisłym, this nie wskazuje już na obiekt globalny. Jest to dobra zmiana, bo nie należy posługiwać się obiektem globalnym, pisząc kod JavaScript.

Wy zazwyczaj będziecie tworzyć aplikacje z wykorzystaniem frameworków, gdzie tryb ścisły jest zazwyczaj domyślny. Dlatego w wielu miejscach zamiast obiektu globalnego pod this będziecie mieli wartość undefined.

Główna idea zmiennego this

Wiemy już, że nie ma znaczenia deklaracja funkcji tylko to, jak została wywołana, na jakim obiekcie następuje wywołanie. Mechanizm ten jest trochę dziwny i inny niż w językach jak Java czy C#. Twórcy jednak chcieli, aby funkcje w JavaScript mogły być użyczane na potrzeby różnych obiektów, raz
zdeklarowana funkcja mogła być użyta kilka razy:

function print() {
  console.log(this.name.toUpperCase(), this.age);
}

const person = {
  name: 'John',
  age: 31,
};

const dog = {
  name: 'Reksio',
  age: 5,
};

print.call(person); // JOHN 31
print.call(dog); // REKSIO 5

Zobaczmy taki kod, gdzie mamy zadeklarowaną funkcję globalną print. Naszym zamysłem jest użycie tej funkcji dla każdego obiektu, który stworzę i będzie miał pole o nazwie name oraz age.

Stworzyłem dwa takie obiekty jak person i dog. Wywołuję funkcję print przez print.call() i w nawiasach okrągłych podaję mój obiekt. O metodzie call będziemy jeszcze rozmawiać później, to wywołanie jednak oznacza, że funkcja print
ma być wywołana w kontekście przekazywanego obiektu. Czyli raz wywołuję funkcję z kontekstem person, a raz z kontekstem dog.

Mógłbym także w każdym obiekcie stworzyć pole print i przypisać funkcję globalną print jako referencję. Wtedy każdy obiekt miałby tą funkcję przypisaną jako pole w obiekcie.

Jednak dzisiaj, najlepszym i najnowocześniejszym rozwiązaniem byłoby stworzenie klasy z polem name , age oraz metodą print. Dzięki klasie tworzylibyśmy sobie obiekty z pełną funkcjonalnością.

Taki był właśnie zamysł twórców, możliwość wykorzystania jednej funkcji w różnych kontekstach, dlatego this w JavaScript zależy od sposobu wywołania funkcji. Otrzymaliśmy więc niezwykle elastyczny mechanizm, który jednocześnie źle wykorzystywany sprawia wiele problemów. Język JavaScript swoją
elastycznością daje ogromne możliwości. Aby dobrze je wykorzystać, musimy dobrze zrozumieć jego działanie.

To jeszcze nie koniec przygody z this w kolejnych odcinkach kolejne przykłady i przypadki działania.

Co warto zapamiętać

  • this jest kontekstem, na którym pracuje dana funkcja czy metoda, jest to zwykły obiekt

  • this zależy od tego, na jakim kontekście wywołana jest funkcja lub metoda, nie ma znaczenia miejsce deklaracji

  • funkcje wywoływane samodzielnie mają kontekst domyślny, czyli obiekt globalny, ale w trybie ścisłym jest to już wartość undefined

  • metoda wywołana na obiekcie będzie miała kontekst tego obiektu, więc this będzie tym, co jest przed kropką

  • zmienny kontekst this miał zapewnić pełną elastyczność kodu i możliwość wywoływania funkcji w różnych kontekstach

    Główny spis treści.