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.
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 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:
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:
- arrow function — użyj
this
z otaczającego scope - wywołanie z new — użyj nowo tworzonego obiektu
call
/apply
/bind
— użyj przekazanego obiektu- wywołanie z kropką, jako metoda — użyj obiektu, na którym została wywołana
- 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!