Zdjęcie artykułu

🔰 Wyjaśniamy this w JavaScript

8m
javascript
podstawy

Intro

Wyjaśnimy jak działa this w JavaScript i sprawdzimy jakie pułapki na nas czekają w codziennej pracy.

Wstęp

Podczas pracy z JavaScript na pewno zdarzyło Ci się mieć problemy z this. Nie jest to temat prosty i oczywisty.

Aby zrozumieć jak działa ten element języka, musimy najpierw zlokalizować problem, później go rozwiązać, a na samym końcu dojść do pewnych wniosków.

W takim razie nie pozostaje mi nic innego, jak zaprosić Cię to lekturki, w której wyjaśnimy wszystko, co konieczne oraz dojdziemy do tego, jak można uniknąć stosowania tego szarlatana.

Pierwszy raz z "this"

Spójrz na poniższy kod, który ma za zadanie stworzyć konkretną listę n-elementów przy wykorzystaniu generycznej klasy List:

Ładowanie

Kod w tym przypadku zachowa się w sposób zgodny z naszymi oczekiwaniami. This będzie wskazywać na instancje klasy, czyli w tym przypadku obiekt zostanie utworzony za pomocą new.

Czy możemy więc założyć, że this to pewnego rodzaju zmienna, która przechowuje referencje do obiektu? To prawda, ale nie do końca. Nie jest to takie proste...

Pierwszy problem z "this"

Wobraź sobie, że przychodzi menago z projektu i mówi, że usuwanie ma być generyczne - typowa sytuacja w projekcie (zmiana wymagań). Nie znasz JS'a szczególnie dobrze. Pierwsze, co przyjdzie Ci do głowy, to przekazanie callbacka do funkcji remove.

Zmieńmy zatem implementacje klasy List:

Ładowanie

Po próbie odpalenia kodu dostaniesz na konsoli coś takiego: Cannot read properties of undefined (reading 'elementIdxToRemove').

Ten błąd mówi, że próbujemy odczytać coś w następujacy sposób: undefined.elementIdxToRemove. Z oczywistych przyczyn to nie może zadziałać.

No, ale jak to! This miało wskazywać na instancje klasy! No i wskazuje. Niestety nie na klasę. W tym przypadku wskazuje na undefined.

Że co?

Znaczenie ma to jak nasza funkcja removeUser jest wywoływana. Dla deklaracji funkcji przekazanych jako argument, this ma wartość undefined. Tak jest to po prostu zaimplementowane. Każda technologia ma jakieś domyślne zachowania.

Zasada 1: Dekleracje funkcji przekazane jako callback mają ustawione this jako undefined.

Jak to naprawić?

Musimy przypisać wartość this do tego, czym powinno być w kontekście naszego kodu, czyli instancją klasy UsersList. Po tym zabiegu zacznie on prawidłowo działać.

Jak to zrobić?

Najprostrzym sposobem będzie wykorzystanie funkcji strzałkowej, która przypisze wartość this na bazie najbliższego zasięgu leksykalnego - w naszym przypadku obiektu stworzonego przez klasę UsersList, a dokładniej this tego obiektu.

Ładowanie

Zasada 2: Funkcje strzałkowe przypisują wartość this, do this tego, co znajduje się w najbliższym zasięgu leksykalnym - nawet jeżeli są przekazane jako callback.

Czym jest zasięg leksykalny?

W zasięgu leksykalnym zakres jest determinowany przez miejsce, w którym zmienna została zadeklarowana w kodzie.

Oznacza to, że zmienna jest dostępna tylko wewnątrz bloku, funkcji lub modułu, w którym została zadeklarowana oraz we wszystkich zagnieżdżonych blokach i funkcjach.

Niezależnie od sposobu wywoływania funkcji, zasięg leksykalny jest określany na podstawie struktury kodu w momencie jego przetwarzania przez silnik JavaScript.

Ładowanie

Nabliższy w zasięgu leksykalnym dla funkcji outerThis jest obiekt window. Natomist dla funkcji innerThis jest to obiekt iHaveMyOwnThis.

Dlaczego zatem console.log dla outerThis pokazał undefined a nie window?

I tu wchodzi w życie kolejna zasada:

Zasada 3: Dla trybu use strict wartość this w przypadku, gdy wskazuje na obiekt globalny przyjmuje wartość undefined.

This może być czymś innym w zależności od środowiska, w jakim uruchamiany jest nasz kod. W przypadku przeglądarki jest to window, a w przypadku Node.js może to być moduł.

Zostało to zaimplementowane w ten sposób, aby uniknąć przykrych niespodzianek przy odnoszeniu się do właściwości obiektu. Może się okazać, że this.nazwaMetody() okaże się tą należącą do obiektu window.

Dzięki temu developer ma możliwość wyłapania potencjalnego problemu w momencie uruchomienia kodu - natychmiastowo widzi wyjątek przy próbie odniesienia się do this, które może wskazywać na obiekt globalny.

Bindowanie niejawne - "implict binding of this"

Ładowanie

W powyższym przykładzie wewnątrz funkcji bike korzystamy z this i logujemy wartość name. W momencie, gdy przypiszemy deklaracje funkcji bike do dowolnego obiektu, ten obiekt staje się this.

Zasada 4: Jeżeli przypiszesz deklarację funkcji, która korzysta z this do obiektu, to ten obiekt staje się this dla tej funkcji.

A jak zachowa się kod, gdy zamienimy deklarację funkcji bike na funkcję strzałkową?

Ładowanie

Dla obydwu przypadków będzie wyjątek: Cannot read properties of undefined (reading 'name)'. Ten sam powód co w pierwszym przykładzie. Zmienna this wskazuje na obiekt window, który przez use strict ma wartość undefined.

Klasy, a "this"

Jeżeli stworzymy obiekt z wykorzystaniem new, to this będzie miało wartość stworzonego obiektu. Dlatego w poniższym kodzie nie mamy problemów.

Ładowanie

Jeżeli wykorzystamy zapis z klasą i użyjemy new, to efekt będzie ten sam. Różnica będzie tylko i wyłącznie w składni.

Ładowanie

Zasada 5: Przy korzystaniu z new wartość this będzie odnosiła się do stworzonego obiektu - nie ma znaczenia tutaj składnia (czy klasa, czy funkcja jako konstruktor).

Dziedziczenie w JavaScript, a "this"

A co z dziedziczeniem? Jaką mamy gwarancję, że czegoś nie popsujemy odnosząc się do this w klasie Vehicle? Aby to udowodnić przekażemy this jako argument do metody repair, a później sprawdzimy jak zachowa się wynik w oparciu o różne miejsca wywołania oraz w oparciu o różne argumenty.

Ładowanie

W pierwszym console.log mamy 2x wartość true. Pokazuje to, że odnosząc się do this wewnątrz klas Bike oraz Vehicle mamy pewność, że this dla klasy rodzica i dziecka jest takie same.

W drugim przypadku mamy false. Metoda repair oczekuje argumentu thisArg, a my go pomijamy.

W trzecim przypadku mamy true. Przekazaliśmy obiekt bike, a wiemy już, że po wykorzystaniu new, wartość this wskazuje na instancje obiektu. Również wiemy, że this klasy rodzica i dziecka wskazuje na to samo. W takim razie this w kontekście dwóch wspomnianych klas jest tym samym, co stworzony obiekt bike, który przekazaliśmy jako argument do jego własnej metody repair.

Zasada 6: Przy dziedziczeniu this odnosi się do tego samego w klasach rodzica i dziecka (tej samej instancji obiektu).

Słowo kluczowe "static"

Tu jest trochę ciekawiej. Spójrz na poniższy kod:

Ładowanie

W przypadku pól oraz metod statycznych this przyjmuje wartość klasy, a nie instancji obiektu - jego jeszcze nie ma i nie będzie. W końcu to metoda statyczna.

Zasada 7: W przypadku pól statycznych this odnosi się do klasy.

Bindowanie jawne - "explicit binding of this"

Do tej pory polegaliśmy na "magicznych" mechanizmach JavaScript, które za nas ustawiają wartość this w zależności od tego, co robimy w kodzie.

Czy jest możliwość ustawienia tego samemu?

Oczywiście, że tak!

Ładowanie

Większość funkcji pozwala na przekazanie czegoś, co opisane jest jako thisArg. W powyższym przykładzie wywołaliśmy funkcję Bike za pomocą call i jako this przekazaliśmy obiekt obj.

Później zrobiliśmy to samo za pomocą apply, która pozwala wywołać funkcję z wieloma argumentami - sterowaliśmy jawnie this.

Można zrobić to nawet dla funkcji takich jak: forEach:

Ładowanie

Zasada 8: Możemy ustawić wartość this jawnie wykorzystując metodę call lub apply, bądź przekazując dodatkowy argument thisArg do metod takich jak forEach.

Bindowanie na chama - "hard/fixed binding of this"

Można zrobić to jeszcze w inny sposób. Jest specjalna metoda bind, która pozwala ustawić wartość this nie w momencie wywołania funkcji, lecz w dowolnym momencie. Zamiast pilnować wartości this dla każdego wywołania, możemy to zrobić tylko raz - w konstruktorze.

Ładowanie

Zasada 9: Możemy ustawić this wykorzystując metodę bind, która tworzy kopię funkcji i pozwala przekazać własne this.

Pozbywanie się "this"

Możemy uniknąć odnoszenia się do this po prostu pisząc kod funkcyjnie. Spójrz na poniższy przykład, w którym kod napisany obiektowo przekształciliśmy do podejścia funkcyjnego i usunęliśmy przy tym this.

Ładowanie

Zamiast odnosić się do this polegamy teraz na mechanizmie domknięcia (closure). Jeżeli interesuje Cię, jak to dokładnie działa, to zapraszam do tego artykułu.

Jeżeli chcesz zgłębić tajniki programowania obiektowego, to czeka na Ciebie jeszcze jeden wpis.

Zasada 10 (dzięki Bogu chyba ostatnia): This możemy usunąć z naszego kodu wykorzystując programowanie funkcyjne oraz mechanizm domknięcia.

Podsumowanie

Jeżeli pozostawimy ustawianie wartości this silnikowi JavaScript, to musimy znać wszystkie zasady. Jest to trochę skomplikowane. Dla świętego spokoju polecam używać zawsze funkcji strzałkowych lub po prostu metod, takich jak call, apply, bind, które pozwalają przekazać/ustawić this.

Drugą opcją, trochę bardziej inwazyjną jest zmiana paradygmatu na funkcyjny.

Cały koncept this to kolejny element języka, który prędzej czy później trzeba dokładnie zrozumieć, aby pisać kod świadomie i nie narażać się na liczne problemy.

Mam nadzieję, że się podobało. Jeżeli tak, to odwiedź nas naLinkedin, gdzie regularnie dzielimy się wiedzą i przekaż swoją opinię.

Sprawdź wytłumaczenie tego konceptu w dokumentacji, na podstawie której powstał ten wpis.

Specjalne podziękowania dla czytelnika Jacka Piętala za pomoc.

Jestem z tych, którzy publikują codziennie!

Mam nadzieję, że mój wpis Ci się spodobał. Jeżeli tak jest, to zapraszam Cię na mój LinkedIn, gdzie publikuję codziennie.

Komentarze

Przejrzyj komentarze artykułu i dodaj swoją opinię.

stworzono: 23-06-2023
zmieniono: 24-06-2023