Skocz do treści

Już wkrótce odpalamy zapisy na drugą edycję next13masters.pl. Zapisz się na listę oczekujących!

this w JS — czyli kilka słów o kontekście wywołania funkcji

Czy kiedykolwiek spotkałaś(-eś) się z błędem w aplikacji, który wynikał z tego, że "this" było ustawione na coś innego, niż się spodziewałaś/eś? Jeśli tak, to nie jesteś jedyna(-y). W swojej karierze programisty miałem okazję występować w roli rekrutera na ponad 160-ciu rozmowach kwalifikacyjnych na stanowiska front-endowe. Jeśli nauczyło mnie to jednego, to tego, że odpowiedź na pytanie „na co wskazuje this?” albo „co to jest kontekst wywołania?” sprawia największą trudność nie tylko kandydatom, ale również doświadczonym programistom w ich codziennej pracy. W tym wpisie pokazuję, że this nie jest tak magiczne jakby się mogło wydawać, a kontekst wywołania da się zrozumieć.

Zdjęcie Wojtek Urbański
Dobry Kod14 komentarzy

Wstęp do "this" w JS

JavaScript to niezwykły język. Jego składnia jest bardzo podobna do tych znanych z C++ czy z Javy. Bardzo wiele wybacza — błąd w JS niekoniecznie skutkuje niezdatnością strony WWW na której wystąpił do użycia, bardzo często działa ona dalej bez najmniejszego problemu. Oba te aspekty bez wątpienia wpłynęły na to, że jest on tak popularny. W końcu wystarczy napisać kilka linijek w pliku *.js i odświeżyć stronę. To wszystko sprawia, że pisać w JS można praktycznie bez żadnego wcześniejszego przygotowania. I dokładnie tak się dzieje.

Z drugiej strony mamy programistów, nie JavaScriptu ogólnie, a konkretnej biblioteki, czy frameworka. Kiedyś było to jQuery, potem Angular.js, a obecnie React.js. Osoby takie często swoją znajomość JS ograniczają właśnie do APi używanego rozwiązania.

Szybko okazuje się jednak, że aplikacja są niewydajne, występują w niej dziwne, nieprzewidziane błędy, a kod jest całkowicie niezrozumiały. Poznanie fundamentów języka, którego używasz nie tylko pozwoli Ci lepiej zrozumieć co pod spodem robi Twoja aplikacja, ale także zapobiec błędom zanim wystąpią — jeszcze na etapie pisania kodu. Dzięki temu pisane przez Ciebie aplikacje będą bardziej wydajne i usiane mniejszą ilością błędów, oraz łatwiejsze w dalszym rozwoju, niezależnie od tego czy ostatecznie korzystasz z Reacta, Vue czy Angulara.

Błąd JavaScript związany z błędnie ustawionym this

Jest to jeden z wielu powodów, dla których warto poznać JavaScript jak najlepiej, na wylot. Po przeczytaniu tego wpisu, mam nadzieję, this w JS będzie jednym z tematów, w których staniesz się ekspertem!

this w programowaniu

W ogólnie pojętym programowaniu obiektowym słowo kluczowe this ma specjalne znaczenie. Wskazuje na obiekt będący kontekstem wykonania. Najprostszym przykładem jest odwołanie się do pola obiektu w jego metodzie. Aby to zrobić napiszesz this.nazwaPola — wtedy kontekstem jest obiekt, na którym wywołana została ta metoda, a this wskazuje właśnie na niego. Tak działa to w językach z klasycznym modelem obiektowości, np. C++, czy Javie.

W JavaScript odpowiedź na pytanie „czym jest this” jest trochę bardziej skomplikowana, ponieważ to, na co wskazuje this zależy nie tylko od sposobu definicji funkcji, ale również od formy i kontekstu wywołania. Doskonale opisuje to moim zdaniem termin “late binding” (choć oczywiście rozumiany inaczej niż late binding w klasycznym OOP).

Domyślny this

Jeśli w kodzie użyjesz słowa kluczowego this poza jakąkolwiek funkcją, to zawsze będzie ono wskazywało na obiekt hostawindow w przeglądarkach oraz module.exports w node.js. Dla uproszczenia, będę odnosił się do niego jako window w dalszej części artykułu:

this; // === window

Podobnie jest w przypadku, gdy wywołasz funkcję bezpośrednio przez referencję do niejthis wskazuje w takiej sytuacji na obiekt window:

function fun() {
  return this;
};

fun(); // === window

Specjalnym przypadkiem jest tu sytuacja, gdy funkcja została zdefiniowana w strict mode. W takim wypadku jedną z konsekwencji jest to, że nie jest już używany domyślny this. Zamiast tego otrzymasz wartość undefined:

function fun() {
  “use strict”;
  return this;
};

fun(); // === undefined

Warto pamiętać, że w kontekście modułów ES2015 (import / export) oraz class, tryb strict jest domyślny.

Metoda obiektu czyli wywołanie z kropką

Przy ustalaniu "this" kolejnym pod względem ważności sposobem wywołania funkcji jest wywołanie jej jako metody obiektu, czyli z kropką po lewej. Obiekt taki nazywany jest obiektem kontekstowym.

var o = {
  a: "o object",
  method: function() {
    return this;
  }
};

o.method(); // === o

Przy takim wywołaniu this wskazuje na obiekt będący bezpośrednio w lewej strony kropki — w tym wypadku o.

var o = {
  a: "o object",
  method: function() {
    return this;
  }
};

var otherO = {
  a: "otherO object",
  method: o.method
}

otherO.method(); // === otherO

Zwróć uwagę, że po przypisaniu referencji do metody do obiektu otherO i wywołaniu jej jako jego metody this wskazuje właśnie na ten obiekt. Zupełnie ignorowany jest fakt, że oryginalnie ta metoda została zdefiniowana w obiekcie o.

Przekazanie referencji to nie wywołanie

Przy tej okazji warto wspomnieć o częstym problemie napotykanym przez programistów. Przekazujesz do jakiejś funkcji referencję do swojej metody tylko po to, aby dowiedzieć się, że this wskazuje na window, a nie oczekiwany obiekt. Dzieje się to np. kiedy chcesz przekazać callback jako then w Promise.

fetch('https://example.com/endpoint').then(o.method); // === window

Powodem takiego zachowania jest fakt, że mimo przekazania referencji do metody z użyciem obiektu kontekstowego, z kropką, fetch (który zwraca Promise) wywołuje Twoją funkcję w sposób samodzielny, czyli przez referencję:

function insideThen(fn) {
  fn();
}

Więcej o Promise'ach możesz przeczytać w tym wpisie:

https://typeofweb.com/kilka-faktow-na-temat-promise/

Promise: 9 rzeczy, których nie wiesz na temat Promise

Każdy programista wcześniej czy później ściera się z problemem asynchroniczności. Jest to temat bardzo złożony, nawet w języku jednowątkowym jakim jest JavaScript. Promise jest abstrakcją, która stara się asynchroniczność ukryć oraz sprawić, aby korzystanie z niej było dla nas przyjemniejsze i bardziej przewidywalne.

Innym, często budzącym zaskoczenie, przypadkiem jest sytuacja, w której funkcja, do której przekazałaś/eś callback celowo zmienia jego this. Idealnym przykładem jest przypięcie funkcji jako callbacka dla zdarzenia DOM, np. kliknięcia. W takim wypadku jako this ustawiany jest element DOM, na którym zaszło zdarzenie. Podobnie zachowuje się biblioteka jQuery.

lnk.addEventListener("click", o.method); // === kliknięty element DOM

Wymuszenie konkretnego obiektu jako kontekstu

Poprzedni przypadek wymagał zmodyfikowania obiektu kontekstowego przez dodanie do niego nowej metody w celu ustawienia konkretnego obiektu jako this metody. Na szczęście istnieją inne mechanizmy pozwalające sprecyzować czym ma być this podczas lub przed wywołaniem funkcji.

Zanim zapoznasz się z tymi mechanizmami, warto zwrócić uwagę na dwie cechy JavaScriptu, które nam to umożliwiają. Po pierwsze, funkcje w JS są tzw. obywatelami pierwszej kategorii (first class citizens) oraz obiektami. Oznacza to, że możesz je przekazywać jako parametry do innych funkcji, oraz że same mogą mieć metody. Po drugie, prototypowa natura JS sprawia, że wszystkie obiekty danego typu mogą mieć dostępne wspólne dla nich pola i metody.

Metody call i apply

Ustawienie konkretnego obiektu jako this podczas wywołania funkcji możliwe jest przy pomocy metod call i apply:

const o = {
  a: "o object",
  method: function() {
    console.log(this, arguments); // wypisuje this oraz przekazane do funkcji argumenty
  }
};

const x = {
  a: "x object"
};

o.method(1, 2); // this === o, [1, 2]
o.method.call(x, 1, 2, 3); // this === x, [1, 2, 3]
o.method.apply(x, [1,2,3]); // this === x, [1, 2, 3]

Jak zapewne zauważyłaś/eś, call i apply różnią się jedynie sposobem w jaki przekazują parametry do wywoływanej funkcji — pierwsza przyjmuje je jako swoje argumenty, druga przyjmuje je jako tablicę, której elementy są kolejno podstawiane. Obie metody za pierwszy parametr przyjmują obiekt, który ma zostać użyty jako this.

Metoda bind

Kolejną dostępną metodą jest bind. W odróżnieniu od poprzedników nie wywołuje on funkcji na miejscu, ale zwraca referencję do funkcji, której this zawsze wskazuje na przekazany obiekt. Kolejne parametry przekazane do bind zostaną podstawione jako pierwsze parametry oryginalnej funkcji podczas wywołania — zostanie więc wykonana częściowa aplikacja (partial application) oryginalnej funkcji:

const m = o.method.bind(x, 1, 2);
m(3,4); // this === x, [1,2,3,4]
setTimeout(m); // this === x, [1,2]

Raz zbindowanej funkcji nie można już nadpisać obiektu kontekstowego w ten sam sposób. Dlatego poniższy kod zadziała inaczej, niż byś tego chciał(a). Zwróć uwagę, że kolejne parametry zostały zaaplikowane mimo zignorowania nowej wartości this.

const m2 = m.bind(o2, 3, 4);
m2(5, 6); // this === x, [1,2,3,4,5,6]

Ignorowanie wartości this

Korzystając z powyższych metod w niektórych wypadkach możesz chcieć zignorować wartość this (np. interesuje Cię jedynie ustawienie domyślny wartości argumentów) lub specjalnie ustawić ją na „nic”. Naturalnym pomysłem, który przychodzi do głowy jest użycie null lub undefined jako wartości pierwszego argumentu:

const ignored = o.method.call(null, 1); // this === window, [1]

W takiej sytuacji, nasz nowy kontekst zostanie jednak zignorowany, a w jego miejsce użyte zostanie… tak, window! Jest to szczególnie ważne, że kod biblioteczny mógłby w ten sposób nadpisać zmienne globalne. Dużo bezpieczniej jest przekazać w takim wypadku pusty obiekt {} lub wynik Object.create(null) czyli pusty obiekt bez prototypu. Obiekt taki jest naprawdę pusty i nie posiada żadnych pól ani metod.

o.method.call(Object.create(null), 1); // this === {}, [1]

Wywołanie z new — funkcje-konstruktory

Funkcję w JS możesz wywołać również jako konstruktor, czyli z użyciem operatora new. To, co dokładnie się dzieje podczas wywołania funkcji z new i jak różni się to od języków takich jak C++, czy Java jest materiałem na osobny post. W tym momencie skup się jedynie na tym, że kiedy funkcja jest wywoływana z new, powstaje nowy, pusty obiekt, który następnie jest ustawiany jako kontekst wywołania tej funkcji:

function Clazz(a,b) {
  this.a = 1;
  this.b = 2;

  return this;
}

Clazz.prototype.method = function() {
  l("Prototype", this);
};

const toBind = { c: 3 };

const instance = new Clazz(); // this === nowy obiekt
const secondInstance = new (Clazz.bind(toBind))()); // this === nowy obiekt

Wywołanie z new ma tak wysoki priorytet, że nadpisuje nawet this ustawiony za pomocą metody bind.

Funkcje strzałkowe — arrow functions oraz this w nich

ECMAScript 6 / 2015, czyli standard na bazie którego powstają implementacje JavaScript, dał nam do dyspozycji nowy sposób definiowania funkcji — funkcje strzałkowe. Główną cechą takich funkcji, oprócz skondensowanej składni, jest fakt, że this jest w nich ustawiany w sposób leksykalny i zależy od miejsca, w którym taka funkcja została zdefiniowana.

Widzisz więc zmianę w porównaniu do standardowego mechanizmu działania this w JavaScript. this w funkcji strzałkowej zawsze wskazuje na to samo, co w funkcji „powyżej”. Oznacza to, że gdy przekazujesz callback do jakiejś biblioteki, albo wywołujesz setTimeout z wnętrza metody w klasie, nie musisz się martwić, że kontekst wywołania this zostanie zgubiony. Będzie on wskazywał na to, na co wskazywałby w tej funkcji (lub na window dla arrow function zdefiniowanej w zakresie globalnym, poza inną funkcją).

function arrowReturner() {
  // this w arrow function poniżej będzie wskazywał na to, na co wskazywałby w tej linijce
  return () => {
    return this;
  };
}

var firstObj = {
  a: 2
};

var secondObj = {
  a: 3
};

var bar = arrowReturner.call(firstObj);

bar(); // this === firstObj
bar.call(secondObj); // this === firstObj
new bar(); // Uncaught TypeError: bar is not a constructor

Funkcje strzałkowe nie dają możliwości nadpisania this w jakikolwiek sposób — ostatecznie zawsze zostaną wykonane z tym oryginalnym. Co ciekawe, jest to zasada tak restrykcyjna, że wywołanie arrow function jako konstruktora kończy się błędem.

Warto jednak pamiętać, że powyższy przykład jest mało życiowy. Głównym zastosowaniem funkcji strzałkowych są wszelkiego rodzaju callbacki i w praktyce raczej nie udostępniasz ich na zewnątrz zwracając referencję.

Podsumowanie

Określenie czym będzie this w wykonywanej funkcji wymaga od Ciebie znalezienia miejsca jej definicji oraz bezpośredniego wywołania. Następnie możesz skorzystać z tych 5 zasad w kolejności od najważniejszej do najmniej ważnej:

  1. arrow function — użyj this z otaczającego scope
  2. wywołanie z new — użyj nowo tworzonego obiektu
  3. call/apply/bind — użyj przekazanego obiektu
  4. wywołanie z kropką, jako metoda — użyj obiektu, na którym została wywołana
  5. domyślnie — undefined w strict mode, obiekt globalny w innym wypadku

Podsumowanie this

Mam nadzieję, że dzięki temu artykułowi odpowiedź na pytanie „czym jest this” stanie się dla Ciebie chociaż trochę łatwiejsza. Jeśli w swojej karierze natrafiłaś(-eś) na jakieś ciekawe lub zabawne problemy związane z wartością kontekstu wywołania this, podziel się nimi w komentarzu!

👉  Znalazłeś/aś błąd?  👈Edytuj ten wpis na GitHubie!

Autor