Na co wskazuje this?
Cześć, witamy Cię w pierwszym artykule na naszej platformie, na pierwszy ogień bierzemy temat, który spędza sen z powiek niejednemu developerowi. Często pojawia się na rozmowach rekrutacyjnych. Do tego stwarza w kodzie wiele niejednoznacznych sytuacji.
Chodzi oczywiście o słowo kluczowe this
Obiekt Window występuje w przeglądarkach, w innym środowisku takim jak np. NodeJS będziemy mieć do czynienia z Global Object
Wywołanie funkcji w JS
Wartość słowa kluczowego this może być różna w trakcie działania aplikacji. Nawet jeżeli jest ono umieszczone dokładnie w tym samym miejscu. Dlaczego? Wynika to z faktu, że istnieje kilka opcji na to jak wywołać funkcję w JavaScript:
-
Default Binding
Klasyczne wywołanie funkcji używające nazwy funkcji oraz nawiasów () W tym typie wywołania this wskazuje na object window (lub global object w środowisku NodeJS) Zobacz poniższy przykład, jest to prosta konstrukcja:
function defaultBindingFunction() { console.log(this) // obiekt window } defaultBindingFunction()
-
Implicit Binding
Jest to wywołanie tak zwanej metody obiektu. Wewnątrz tej metody this wskazuje na obiekt, z którego została wywołana. Żeby ułatwić sobie zrozumienie, w tym przypadku można patrzeć „na to, co jest z lewej strony kropki”
Zobrazujmy to krótkim przykładem:
const testObj = { name: "Instytut Fullstack", objMethod () { console.log(this) } } testObj.objMethod(); // This wewnątrz objMethod wskazuje na testObj (jest na lewo od kropki) => Implicit Binding
-
Explicit Binding
Żeby wywołać funkcje w sposób explicit, należy posłużyć się jedną z funkcji call(), apply() lub bind()
Warto również zaznaczyć, że call() i apply() wywołują funkcję, a bind tylko ją zwraca wraz z nowym kontekstem. Aby, z tego skorzystać należy ją jeszcze samemu uruchomić Zobaczmy, jak to wygląda w praktyce:
const newContextObj = { name: "New context for this" } function testFunction(arg1, arg2) { // Jeśli nie podamy argumentów do funkcji to będą miały wartość undefined console.log(arg1); console.log(arg2); console.log(this); } // This wewnątrz funkcji będzie wskazywało na newContextObj a argumenty funkcji będą undefined => Explicit Binding testFunction.call(newContextObj); testFunction.call(newContextObj, "argument 1", 2); // przekazanie argumentów // Apply Działa tak samo jak call, ale argumenty przekazuje się nie po przecinku tylko jako array testFunction.apply(newContextObj); testFunction.apply(newContextObj, ["argument 1", 2]); // przekazanie argumentów // Bindowanie funkcji const bindedFunction = testFunction.bind({testContext: 'bindedProp'}); bindedFunction();
-
Słowo kluczowe new
Jest to wywołanie funkcji jako klasy. Wraz z użyciem słowa kluczowego new dzieje się kilka rzeczy, do których opisania potrzebny jest osobny artykuł.
Jedną z nich jest właśnie „wywołanie funkcji i ustawienie słowa kluczowego this, aby wskazywało na nowo utworzony obiekt”. Spójrzmy, jak wygląda sytuacja, gdy spróbujemy wywołania z operatorem new:
class InstytutFullstack { testProperty = 'testProperty' constructor() { console.log("create Instytut instance") console.log(this) } printThis() { console.log(this) } } const instytutInstance = new InstytutFullstack() instytutInstance.printThis()
Zarówno this wewnątrz konstruktora, jak i funkcji printThis wskazują na nowo utworzoną instancję klasy InstytutFullstack
A żeby tego wszystkiego było mało powyższe zasady nie działają dla arrow functions…
This wewnątrz arrow function
Arrow function to specyficzny przypadek, ponieważ funkcja strzałkowa nie definiuje swojego this. Korzysta z tego, które występuje w jej zakresie leksykalnym
Mówiąc po ludzku, szuka this w miejscu, w którym została zdefiniowana w podobny sposób jak JS szuka zwykłych zmiennych
Co więcej, na arrow function nie da się wykonać operatora new więc cała teoria klas tutaj odpada
Przyjrzyjmy się poniższemu przykładowi:
const objWithArrow = { prop1: '1', prop2: '2', arrowMethod: () => { console.log(this); // Tutaj this wskazuje na obiekt window }, classicFunction() { console.log(this); // Tutaj this wskazuje na objWithArrow // IIFE da się też robić przy użyciu arrow functions (() => { console.log(this); // Tutaj też this wskazuje na objWithArrow, bo arrow function jest wewnątrz zwykłej funkcji i przejmuje od niej this })(); } } // Zasada Implicit binding nie działa dla arrow functions objWithArrow.arrowMethod(); // W tym przypadku funkcja strzałkowa wewnątrz classicFunct korzysta z kontekstu classicFunct więc this wskazuje na objWithArrow objWithArrow.classicFunction();
Jak widać, this umieszczone wewnątrz metody obiektu utworzonej jako arrow function wskazuje na window zamiast na ten obiekt.
Otaczający ją zasięg leksykalny to po prostu zasięg globalny stąd this wskazuje na window.
W przypadku metody classicFunct jest to zwykła funkcja, która definiuje swoje this. Wywołanie w jej wnętrzu arrowFunction sprawi, że JS będzie szukał najbliższej definicji this i znajdzie ją właśnie wewnątrz classicFunction
Można więc wysnuć prosty wniosek, że this wewnątrz arrow functions zachowuje się trochę „jak zwykła zmienna”, która nie zależy od sposobu wywołania, ale od miejsca jej deklaracji
Użycie this w event handlerach
W kontekście event handlerów możemy rozróżnić:
- Inline event handlery
- Klasyczne funkcje podpięte w skrypcie
- Arrow functions podpięte w skrypcie
<button onclick="inlineClickHandlerFunction(this)">Inline Event Handler</button> <button id="testBtn">Click Me</button> <script> function inlineClickHandlerFunction(arg1) { console.log(arg1) // wskazuje na klikany element console.log(this) // obiekt window } function clickHandlerFunction() { console.log(this) } document.getElementById("testBtn").addEventListener("click", clickHandlerFunction) // wskazuje na element document.getElementById("testBtn").addEventListener("click", () => { console.log(this) // obiekt window }) </script>
W przypadku inline handlerów musimy ręcznie przekazać this jako argument co nie jest zbyt wygodne
Podpinając event w skrypcie, jeśli chcemy mieć dostęp do elementu, należy użyć klasycznej funkcji, a jeśli nam na tym nie zależy to wystarczy arrow function
Jak strict mode wpływa na this
W przypadku defaultBinding, jeśli uruchomimy aplikację w trybie strict this będzie wskazywało na undefined zamiast obiektu window
Dodajmy jedną linijkę do jednego z wcześniejszych przykładów:
"use strict" function defaultBindingFunction() { console.log(this) } defaultBindingFunction()
Naszym oczom ukaże się wartość undefined
W innych sytuacjach strict mode nie ma wpływu na to, co wskazuje this
Pozostałe przypadki
This z najwyższego poziomu kodu (bez zagnieżdżeń w żadnej funkcji ani obiekcie) będzie wskazywało na obiekt window
Warto zauważyć, że jeśli w ES Module wywołamy funkcję w taki sam sposób to this wewnątrz niej wskaże undefined
Jest to spowodowane tym, że ES moduły domyślnie działają w wyżej opisanym trybie strict
Podsumowanie
W tym artykule pokazaliśmy Ci jak poradzić sobie z JS’owym zagadnieniem starym jak Internety
Oczywiście znajdą się jeszcze pewnie kolejne skrajne przypadki. Jednak ich pokrycie sobie odpuściliśmy ze względu na to, że nie są one zbyt częste w codziennej pracy.
Jeśli zauważyłeś błąd lub coś, czego nie uwzględniliśmy, śmiało napisz do nas :)
Bonus
Na sam koniec przetestuj swoją wiedzę i odgadnij, na co wskazuje this w każdym z poniższych console.logów
Odpowiedzi nie są oczywiste i niejeden doświadczony programista może mieć z nimi problem
var length = 27 // pytanie z gwiazdką, a co gdyby był tutaj const zamaist vara? function fnToPassAsArgument() { console.log(this.length); } const instytutFullstack = { length: 22, name: "Instytut Fullstack", callThis: function () { console.log(this) }, nested: function() { console.log(this); (function () { console.log(this) })() }, anotherOne: function(fn) { fn(); arguments[0](); } } instytutFullstack.callThis() // jaki wynik? const differentCaller = instytutFullstack.callThis differentCaller() // a tutaj? instytutFullstack.nested() // dwa logi, ale czy takie same? instytutFullstack.anotherOne(fnToPassAsArgument, 1); // na koniec jest gruba sprawa, dasz radę?
I jak, udało się?