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ć.

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 hosta — window 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 niej — this 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 thenPromise.

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:

9 rzeczy, których nie wiesz na temat Promise

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 callapply:

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ś, callapply 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!

Nie wysyłamy spamu, tylko wartościowe informacje. W każdej chwili możesz się wypisać klikając „wypisz się” w stopce maila.

Subscribe
Powiadom o
guest
19 komentarzy
Most Voted
Newest Oldest
Inline Feedbacks
View all comments
Natalia
2 lat temu

Świetny artykuł! Aczkolwiek muszę go przeczytać jeszcze ze 4 razy żeby odczarować this 🙂

Michał z typeofweb.com
Admin
Michał z typeofweb.com (@michal-miszczyszyn)
2 lat temu
Reply to  Natalia

Ale planujemy więcej podobnych artykułów, w których bierzemy jakiś „prosty” temat i wchodzimy w niego wgłąb tak bardzo, że jednak okazuje się, że nie jest taki prosty 😀

Michał z typeofweb.com
Admin
Michał z typeofweb.com (@michal-miszczyszyn)
2 lat temu
Reply to  Natalia

Kwestia doświadczenia!

Tomasz Sochacki
2 lat temu

Jako uzupełnienie świetnego artykułu dodam tylko taki mały przykład:

//Składnia metody Array.prototype.map:
//arr.map( callback function [, thisObject] );

const arr = [5,10];
const thisObj = { x: 100 };

arr.map( ( number ) =>  number * this.x, thisObj ); [NaN, NaN]

arr.map( function( number ) { 
    return number * this.x;
}, thisObj ); [500, 1000]

Oczywiście w strict mode dostaniemy błąd, dlatego celowo nie owijałem tego w IIFE z ‚use strict’. W praktyce w sumie nie korzystałem jeszcze ze wskazywania drugiego argumentu dla metod Array.prototype ale jak już jesteśmy w temacie this to można wspomnieć, że użycie arrow function nie pozwala na to. Jest to oczywiście wynikiem precyzyjnego wskazywania this dla arrow function co opisałeś i nie ma od tego wyjątków 🙂
Pozdrawiam

wojtiku
wojtiku
2 lat temu

To kolejny – świetny – przykład pułapki w jaką można wpaść w odniesieniu do this. Dzięki! Przy jego okazji wspomniałbym o dwóch rzeczach jakie przychodzą mi do głowy.

Z jednej strony w praktyce do `map` pewnie przekazalibyśmy właśnie arrow function, ale bez wskazania drugiego argumentu. Będąc w jakimś komponencie czy instancji „klasy” najczęściej chcemy użyć `this` z kontekstu wyżej. W takim wypadku `this` wskazywałby na spodziewaną instancję. Dokładnie po to powstały arrow functions.

Druga rzecz, o której warto pamiętać to fakt, że w ogromnej większości środowisk produkcyjnych taki kod by zadziałał. Dlaczego? Osobiście nie znam nikogo, kto używa elementów języka dostępnych od ES6/2015 bez transpilacji do ES5. W praktyce więc taki kod, zanim dotrze do przeglądarki, będzie zawierał zwykłą funkcję i wszystko zadziała tak jak… dla zwykłej funkcji :).

Warto o tym pamiętać gdyż może się okazać, że za kilka lat nasza aplikacja nadal będzie musiała być transpilowana… aby działać poprawnie. Właśnie przez takie „kwiatki”.

Michał z typeofweb.com
Admin
Michał z typeofweb.com (@michal-miszczyszyn)
2 lat temu
Reply to  wojtiku

Akurat teraz (ale jestem niemal pewien, że kiedyś tak nie było) babel robi to poprawnie 🙂 Tzn. podany kod skompiluje się do: `number * undefined.x;` (bo babel zawsze kompiluje do „use strict”) co zakończy się wyjątkiem.

Tomasz Sochacki
2 lat temu

A faktycznie, rzadko sprawdzam sobie jak wygląda kod po babelowaniu (ale ze mnie polonista :P) ale faktycznie, daje undefined.
Tak swoją drogą to ciekawym projektem musiało być stworzenie Babela i testów do niego 🙂

Michał z typeofweb.com
Admin
Michał z typeofweb.com (@michal-miszczyszyn)
2 lat temu

Oj, to na pewno — niezłe wyzwanie!

Tomasz Sochacki
2 lat temu
Reply to  wojtiku

heh, a wiesz że nie pomyślałem o tym co wypluwa babel 🙂 czlowiek codziennie używa i z czasem rutyna daje o sobie znać… ale zawsze mamy tez node gdzie nie zawsze trzeba robic transpilacje, chyba ze np. robimy ssr react itp.

trackback

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

trackback

[…] Początkowo w postaci wpisów gościnnych (jak na przykład ten od Wojtka: „this w JS — czyli kilka słów o kontekście wywołania funkcji”), ale docelowo na zasadach współpracy długofalowej. Zdecydowałem się […]

trackback

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

Ola Gruchała
Ola Gruchała
1 rok temu

Pytanie do tego kawałka:

const m2 = m.bind(o2, 3, 4);

rozumiem że, tworzymy nową funkcję m2, w której za pomocą metody .bind rozszerzamy m o dwa kolejne argumenty (3, 4). A czym jest o2 ?? bo nie rozumiem dlaczego piszesz, że ten kod zadziała inaczej niż bym tego chciała.

Dalej wywołujesz m2 z dwoma argumentami:

m2(5, 6); // this === x, [1,2,3,4,5,6]

czyli wszystko pasuje…, gdyby tylko zamiast o2 było x w poprzedniej linii?

Nie wiem czy jasno opisałam swoją wątpliwość.

Michał z typeofweb.com
Admin
Michał z typeofweb.com (@michal-miszczyszyn)
1 rok temu
Reply to  Ola Gruchała

Pierwszy argument do .bind() to zawsze nowa wartość „this”. Jednak tę wartość da się ustawić w ten sposób tylko jeden raz.
Dlatego w tym przykładzie this === x zamiast o2

pp
pp
1 rok temu

Dobry artykuł, ale pisany przez doświadczonego programistę dla doświadczonego programisty, czyli nie dla mnie :/ Poszukam kolejnej strony o ‚this’.

Michał z typeofweb.com
Admin
Michał z typeofweb.com (@michal-miszczyszyn)
1 rok temu
Reply to  pp

A masz jakieś pytania? 🙂 Śmiało pytaj!

Piotr Goławski
Piotr Goławski
1 rok temu

Bardzo pomocny art, dziękuje!

trackback

[…] „This” w funkcjach w JavaScripcie to skomplikowana sprawa! Na szczęście jQuery sprawia, że bindowanie, czyli zmiana kontekstu, staje się banalne: […]

trackback

[…] Więcej na temat kontekstu wywołania funkcji dowiesz się na: typeofweb.com. […]