Moduł5 - Zajęcia 9 - Słowo kluczowe this

Kontekst wykonania funkcji

Można śmiało powiedzieć, że słowo kluczowe this jest jednym z najbardziej mylących pojęć JavaScript na początku nauki. Początkujący często używają this przez metodę prób i błędów, dopóki skrypt nie zadziała.

Kontekst w JavaScript jest jak kontekst w zdaniu:

  • Maciej biegnie szybko, ponieważ Maciej próbuje złapać pociąg.
  • Maciej biegnie szybko, ponieważ on próbuje złapać pociąg.

Drugie zdanie jest bardziej lakoniczne. Podmiotem zdania jest Maciej i możemy powiedzieć, że kontekstem zdania jest Maciej, ponieważ to on jest w centrum uwagi w tym konkretnym zdaniu. Nawet zaimek "kto" odnosi się do Macieja.

W ten sam sposób obiekt może być bieżącym kontekstem wykonywania funkcji.

            // Maciej biegnie szybko, ponieważ Maciej próbuje złapać pociąg.
            const maciej = {
              username: "Maciej",
              showName() {
                console.log(maciej.username);
              },
            };
            maciej.showName();
          

Odwołanie do właściwości obiektu wewnątrz metod przy użyciu nazwy samego obiektu jest podobne do używania Maciej zamiast on.

Zastrzeżone słowo kluczowe this może być użyte wewnątrz funkcji. Podczas wykonywania funkcji w this dostępne jest odwołanie do obiektu, w kontekście którego została wywołana. W ten sposób w ciele funkcji możemy uzyskać dostęp do właściwości i metod tego obiektu.

            // Maciej biegnie szybko, ponieważ on (this) próbuje złapać pociąg.
            const maciej = {
              username: "Maciej",
              showName() {
                console.log(this.username);
              },
            };
            maciej.showName();
          

Spójrzmy na przykład z kolekcją książek.

            const bookShelf = {
              authors: ["Bernard Cornwell", "Robert Sheckley"],
              getAuthors() {
                return this.authors;
              },
              addAuthor(authorName) {
                this.authors.push(authorName);
              },
            };

            console.log(bookShelf.getAuthors());// ["Bernard Cornwell", "Robert Sheckley"]
            bookShelf.addAuthor("Tanith Lee");
            console.log(bookShelf.getAuthors());// ["Bernard Cornwell", "Robert Sheckley", "Tanith Lee"]
          

Metody getAuthors i addAuthor to funkcje (metody obiektowe), które są wywoływane w kontekście obiektu bookShelf. Podczas ich wykonywania do this zapisywane jest odwołanie do obiektu bookShelf i możemy przy pomocy this odwołać się do jego właściwości i metod.

Zasady definiowania this

Warto nauczyć się tylko jednej głównej zasady definiowania this - wartość kontekstu wewnątrz funkcji (nie strzałkowej) będzie określana nie w momencie jej utworzenia, ale w momencie wywołania. Oznacza to, że wartość this jest określana przez sposób wywołania funkcji, a nie przez miejsce, w którym została zadeklarowana.

this w zasięgu globalnym

W zasięgu globalnym, jeśli skrypt nie jest wykonywany w trybie ścisłym, this odnosi się do obiektu window. W trybie ścisłym wartość this, w zasięgu globalnym to undefined.

            function foo() {
              console.log(this);
            }
            foo();// window bez "use strict" i undefined z "use strict"
          

this w metodzie obiektu

Jeśli funkcja została wywołana jako metoda obiektu, to kontekst będzie odnosił się do obiektu, którego częścią jest metoda.

            const maciej = {
              username: "Maciej",
              showThis() {
                console.log(this);
              },
              showName() {
                console.log(this.username);
              },
            };

            maciej.showThis();// {username: "Maciej", showThis: ƒ, showName: ƒ}
            maciej.showName();// 'Maciej'
          

Spójrzmy na bardziej złożony przykład, aby lepiej zrozumieć tę zasadę.

  • Najpierw utwórzmy funkcję w zasięgu globalnym i wywołajmy ją.
  • Następnie przypiszmy ją do właściwości obiektu i wywołajmy jako metodę tego obiektu.
            function showThis() {
              console.log("this in showThis: ", this);
            }

            // Wywołujemy w kontekście globalnym
            showThis();// this in showThis: Window

            const user = {
              username: "Mango",
            };

            // Zapisujemy link do funkcji do właściwości obiektu
            // Zauważ, że to nie jest wywołanie - nie ma ()
            user.showContext = showThis;

            // Wywołaj funkcję w kontekście obiektu
            // this wskaże na bieżący obiekt, w kontekście
            // którego odbywa się wywołanie, a nie na obiekt globalny.
            user.showContext();
            // this in showThis as show Context: {username: "Mango", showContext: ƒ}
          

this w funkcjach callback

Podczas przekazywania metod obiektowych jako funkcji wywołania zwrotnego kontekst nie jest zachowywany. Wywołanie zwrotne to odwołanie do metody, które jest przypisywane jako wartość parametru wywoływanego bez udziału obiektu.

            const customer = {
              firstName: "Jacob",
              lastName: "Mercer",
              getFullName() {
                return `${this.firstName} ${this.lastName}`;
              },
            };

            function getMessage(callback) {
              // callback() to wywołanie metody getFullName bez obiektu
              console.log(`Przetwarzanie żądania od ${callback()}.`);
            }
            getMessage(customer.getFullName);// Wystąpi błąd podczas wywoływania funkcji
          

Rozwiązanie tego problemu zostanie omówione w części poświęconej metodzie bind() i metodom obiektowym.

this w funkcjach strzałkowych

Funkcje strzałkowe nie mają własnego this. W przeciwieństwie do zwykłych funkcji, nie możesz zmienić wartości this wewnątrz funkcji strzałkowej po jej deklaracji.

Kontekst wewnątrz funkcji strzałkowej jest określony przez miejsce jej deklaracji, a nie wywołanie i odwołuje się do kontekstu funkcji nadrzędnej.

Funkcje strzałkowe również ignorują obecność trybu ścisłego. Jeśli strzałka pamięta globalny kontekst, this będzie zawierało odniesienie do window niezależnie od tego, czy skrypt jest wykonywany w trybie ścisłym, czy nie.

            const showThis = () => {
              console.log("this in showThis: ", this);
            };

            showThis();// this in showThis: window

            const user = {
              username: "Mango",
            };
            user.showContext = showThis;

            user.showContext();// this in showThis: window
          

Ograniczając funkcje strzałkowe do stałego kontekstu this, silniki JavaScript mogą je lepiej optymalizować, w przeciwieństwie do zwykłych funkcji, w których wartość this można zmienić.

Poniższy przykład nie jest bardzo praktyczny, ale pokazuje, jak działa kontekst funkcji strzałkowych. Wartość kontekstu jest pobierana z zakresu nadrzędnego.

            const hotel = {
              username: "Resort hotel",
              showThis() {
                const foo = () => {
                  // Funkcja strzałkowa zapamiętuje kontekst podczas deklaracji,
                  // z zasięgu nadrzędnego
                  console.log("this in foo: ", this);
                };

                foo();
                console.log("this in showThis: ", this);
              },
            };

            hotel.showThis();
            // this in foo: {username: 'Resort hotel', showThis: ƒ}
            // this in showThis: {username: 'Resort hotel',showThis: ƒ}
          

Metody funkcyjne

Zdarzają się sytuacje, w których funkcję trzeba wywołać w kontekście obiektu, a funkcja nie jest jego metodą. Aby to zrobić, funkcje posiadają metodycall, apply i bind.

Metoda call()

Metoda call wywoła funkcję foo, a dzięki podanym argumentom, this będzie zawierało odwołanie do obiektu obj, a także przekazane zostaną argumenty arg1, arg2 itd.

            foo.call(obj, arg1, arg2, ...)
          

Pokażmy powyższy zapis w praktyce

            function greetGuest(greeting) {
              console.log(`${greeting}, ${this.username}.`);
            }

            const mango = {
              username: "Mango",
            };
            const poly = {
              username: "Poly",
            };

            greetGuest.call(mango, "Witaj");// Witaj, Mango.
            greetGuest.call(poly, "Witaj");// Witaj, Poly.
          

Metoda apply

Metoda apply jest analogiczna do metody call z tą różnicą, że składnia przekazywania argumentów wymaga tablicy z argumentami, nawet jeśli istnieje tylko jeden argument.

            foo.call(obj, arg1, arg2, ...)
            foo.apply(obj, [arg1, arg2, ...])
          

Metoda apply wywoła funkcję foo, a dzięki podanym argumentom, this miało zawierało odwołanie do obiektu obj, a także przekazane zostaną elementy tablicy jako osobne argumenty arg1, arg2 itd.

            function greetGuest(greeting) {
              console.log(`${greeting}, ${this.username}.`);
            }

            const mango = {
              username: "Mango",
            };
            const poly = {
              username: "Poly",
            };

            greetGuest.apply(mango, ["Witaj"]);// Witaj, Mango.
            greetGuest.apply(poly, ["Witaj"]);// Witaj, Poly.
          

Metoda bind()

Metody call i apply wywołują funkcję "na miejscu", czyli natychmiast. Ale w przypadku funkcji callback, gdy konieczne jest nie natychmiastowe wywołanie funkcji, ale przekazanie do niej linku z kontekstem do powiązania, stosowana jest metoda bind.

            foo.bind(obj, arg1, arg2, ...)
          

Metoda bind tworzy i zwraca kopię funkcji foo z powiązanym kontekstem obj i ewentualnie z argumentami arg1, arg2 itd. Kopia funkcji może być przekazana gdziekolwiek i wywołana, kiedy tylko chcesz.

Zobaczmy to w praktyce:

            function greet(clientName) {
              return `${clientName}, witaj w «${this.service}».`;
            }

            const steam = {
              service: "Steam",
            };
            const steamGreeter = greet.bind(steam);
            steamGreeter("Mango");// "Mango, witaj w «Steam»."

            const gmail = {
              service: "Gmail",
            };

            const gmailGreeter = greet.bind(gmail);
            gmailGreeter("Poly");// "Poly, witaj w «Gmail»."
          

bind() i metody obiektowe

Jak już wspomnieliśmy, podczas przekazywania metod obiektowych jako funkcji callback, kontekst nie jest zachowywany. Callback to odwołanie do metody, które jest przypisywane jako wartość parametru wywoływanego bez kontekstu obiektu

            const customer = {
              firstName: "Jacob",
              lastName: "Mercer",
              getFullName() {
                return `${this.firstName} ${this.lastName}`;
              },
            };

            function makeMessage(callback) {
              // callback() to wywołanie metody getFullName bez obiektu
              console.log(`Przetwarzanie żądania od ${callback()}.`);
            }

            makeMessage(customer.getFullName);// Wystąpi błąd podczas wywoływania funkcji
          

W trybie ścisłym, wartość this w metodzie getFullName, wywołanej jako funkcja wywołania zwrotnego callback(), będzie undefined. Podczas odwołania do właściwości firstName i lastName wystąpi błąd, ponieważ undefined nie jest obiektem.

Bez trybu strict kod również nie zadziała, ponieważ this w tym kontekście będzie referencją do obiektu window więc otrzymamy niepoprawny komunikat i jednocześnie trudny do odnalezienia błąd w naszym kodzie.

Metoda bind służy do wiązania kontekstu podczas przekazywania metod obiektów jako funkcji wywołania zwrotnego. Przekażmy przez wywołanie zwrotne nie oryginalną metodę getFullName, ale jej kopię z przypisanym kontekstem do obiektu customer.

            // ❌ Źle!
            makeMessage(customer.getFullName);// Wystąpi błąd podczas wywoływania funkcji

            // ✅ Dobrze :)
            makeMessage(customer.getFullName.bind(customer));// Przetwarzamy żądanie od Jacob Mercer.