Trochę więcej o this

W poprzednich rozdziałach poznaliśmy tworzenie własnych obiektów oraz słowo kluczowe this, które "wskazywało na dany obiekt".

Poznanie działania this jest jedną z kluczowych spraw w Javascript, dlatego przypatrzmy mu się dokładniej.

Słowo this wskazuje na tak zwany execution context, czyli miejsce, kontekst, w którym wywoływany jest dany kawałek kodu.

Global context

Jeżeli nasz kod uruchamiany jest z najwyższego poziomu (czyli nie umieściliśmy go w żadnej funkcji), domyślnym kontekstem jest obiekt globalny, którym dla przeglądarek jest obiekt window (dla środowiska Node.js jest to obiekt module.exports (patrz rozdział o modułach))

Function context

Gdy odpalamy jakąś funkcję (poza funkcją strzałkową), w jej wnętrzu wartość this zależna jest od tego jak i gdzie ta funkcja jest wywoływana. Inaczej mówiąc następuje wiązanie this do jakiejś wartości.

Jeżeli funkcja nie jest metodą żadnego obiektu, wtedy w jej wnętrzu this będzie wskazywało na obiekt globalny:


console.log(this); //window

function test() {
    console.log(this); //window
}

test();

Wyjątkiem jest tutaj zastosowanie strict mode lub używanie modułów ES6 które automatycznie włączają ten tryb. Wówczas zarówno w przeglądarce jak i w Node.js wewnątrz odpalanej funkcji this będzie domyślnie wskazywać na undefined:


    "use strict;"

    console.log(this); //window dla przeglądarki lub module.exports dla Node.js

    function test() {
        console.log(this); //undefined
    }

    test();
    

Jeżeli jednak dana funkcja odpalana jest jako metoda danego obiektu, wtedy w jej wnętrzu this wskazuje zazwyczaj właśnie na ten obiekt:


const ob = {
    name : "pies",

    show() {
        console.log(this); //ob
    }
}

ob.show();

Tak samo dzieje się w przypadku innych obiektów:


Math.max(...) //this w metodzie max() wskazuje na Math

[1,2,3,4].push(5) //this w metodzie push() wskazuje na tablicę

window.alert() //this w metodzie alert() wskazuje na window

"Ala ma kota".toUpperCase() //this w metodzie toUpperCase() wskazuje na tekst

Powyższe zasady tyczą się każdego poziomu zagnieżdżenia:


function testA() {
    console.log(this); //window
}
testA();

const pet = {
   name : "pies",

   show() {
        console.log(this); //pet

        //poniższa funkcja mimo, że zagnieżdżona, NIE JEST metodą obiektu pet
        //ani metodą poniższego obiektu obj
        //zauważ zresztą jak ją odpalamy. Nie poprzedzamy ją nazwą obiektu pet.test()
        function test() {
            console.log(this); //window
        }
        test();

        const obj = {
            show() {
                console.log(this); //obj, bo ta funkcja jest metodą obiektu obj
            }
        }
        obj.show();
   }
}

pet.show();

Zmieniający się this

Wiedząc te wszystkie rzeczy, przejdźmy do kolejnego przykładu. Mamy więc obiekt, w który tworzymy metodę bindButton(), w której to pobieramy jakiś przycisk ze strony i podpinamy mu kliknięcie:


const ob = {
    pets : ["kot", "pies", "chomik"],

    bindButton() {
        console.log(this); //ob
        console.log(this.pets); //["kot", "pies", "chomik"]

        //pobieramy przycisk
        const btn = document.querySelector("button");

        //podpinamy mu nasłuchiwanie kliknięcia
        btn.onclick = function() {
            console.log(this); //button

            //?????? - jak się odwołać do powyższej tablicy pets?
            //skoro this wskazuje na btn
            console.log(this.pets);
        });
    }
}

ob.bindButton();

Po pierwsze czemu po kliknięciu na przycisk this z 13 linii wskazuje na btn?. Przeanalizuj poniższy kod:


const ob = {
    name : "Karol",
    onclick() {
        console.log(this.name);
    }
}

ob.age = 10; //poza ciałem obiektu mogę dodawać mu nowe rzeczy
ob.name = "Marcin"; //ale też spokojnie mogę coś nadpisywać
ob.onclick = function() { //tutaj np. podstawiam nową funkcję pod klucz onclick, nadpisując to co istniało tam wcześniej
    console.log(`Nazywam się ${this.name} i mam ${this.age}`);
}

Element/obiekt btn pobrany ze strony także ma metodę onclick, pod którą podstawiłem własną funkcję. Funkcja taka jest odpalana na kliknięcie (a to już robi wbudowany mechanizm). Skoro jest to metoda przycisku, to w jej wnętrzu this wskazuje właśnie na ten przycisk czyli w naszym przypadku obiekt btn.

Jak sprawić by po kliknięciu na przycisk this wskazywało na obiekt ob a nie btn?

Jest na to kilka sposobów.

Dodatkowa zmienna wskazująca na właściwe this

Pierwszym z nich jest stworzenie dodatkowej zmiennej, która będzie wskazywała na obiekt. Dzięki temu możemy się do niej odwoływać w funkcji, w której zmienione zostało this:


const ob = {
    pets : ["kot", "pies", "chomik"],

    bindButton() {
        console.log(this); //ob
        const that = this;

        const btn = document.querySelector("button");
        btn.addEventListener("click", function() {
            console.log(this); //button
            console.log(that); //ob
            console.log(that.pets); //["kot", "pies", "chomik"]
        });
    }
}

ob.bindButton();

Nie jest to może najbezpieczniejsza metoda na świecie, ale w wielu przypadkach się sprawdza, a i jest całkiem logiczna w użyciu. To taki klasyk używany przez wiele skryptów.

Funkcja bind()

Drugim sposobem jest skorzystanie z funkcji bind(newThis, param1*, param2*...), która zwraca nam daną funkcję ze zmienionym w jej wnętrzu wiązaniem this.

Jako pierwszy parametr podajemy to co będzie podstawione pod this wewnątrz zwracanej funkcji, natomiast kolejne parametry to wartości, które przyjmuje modyfikowana funkcja - oczywiście jeżeli ich oczekuje.


//to nie jest metoda żadnego obiektu więc this === window
const myFunction = function() {
    console.log(this);
}

myFunction(); //window

const myNewFn = myFunction.bind({x : 10});
myNewFn(); //{x : 10}

const ob = {
    name : "Marcin",

    show(txt) {
        console.log(`${this.name} ${txt}`);
    }
}

ob.show("jest ok"); //Marcin jest ok

const show = ob.show.bind({name : "Leszek"}, "też jest ok");
show(); //Leszek też jest ok

const ob = {
    pets : ["kot", "pies", "chomik"],

    bindButton() {
        const btn = document.querySelector("button");
        btn.addEventListener("click", function() {
            console.log(this.pets);
        }.bind(this));
    }
}

ob.bindButton();

const ob = {
    pets : ["kot", "pies", "chomik"],

    showPets() {
        console.log(this.pets);
    },

    bindButton() {
        const btn = document.querySelector("button");
        btn.addEventListener("click", this.showPets.bind(this));
    }
}

ob.bindButton();

Funkcja strzałkowa

W dzisiejszych czasach w podobnych sytuacjach najczęściej używana jest funkcja strzałkowa, która poza skróconym zapisem powoduje, że wewnątrz funkcji nie następuje wiązanie this, co oznacza, że this się nie zmienia. Osobiście lubię to nazywać "this jest brane z zewnątrz"

Jeżeli więc w powyższym kodzie podepniemy zdarzenie za pomocą funkcji strzałkowej, unikniemy problemu zmiany this:


const ob = {
    pets : ["kot", "pies", "chomik"],

    showPets() {
        console.log(this.pets);
    },

    bindButton() {
        const btn = document.querySelector("button");

        btn.addEventListener("click", function() {
            this.showPets(); //błąd bo this wskazuje na btn, a btn nie ma takiej metody
        });

        btn.addEventListener("click", () => {
            this.showPets(); //["kot", "pies", "chomik"]
        });

        //lub krócej
        btn.addEventListener("click", () => this.showPets());
    }
}

ob.bindButton();

Funkcja strzałkowa wraz z bind()

Funkcja strzałkowa jest bardzo fajnym sposobem na radzenie sobie z this, ale nie zawsze będzie wystarczającym rozwiązaniem.

Niestety muszę tu przejść do tematów, które jeszcze nie były przez nas omawiane, a dokładniej podpinania i odpinania zdarzeń.

Gdy podpinamy nasłuchiwanie zdarzenia do danego elementu, podajemy funkcję, która będzie odpalana w momencie wystąpienia takiego zdarzenia. Tak jak było to pokazane powyżej, domyślnie this w takiej funkcji wskazuje na dany element.


const ob = {
    pets : ["kot", "pies", "chomik"],

    showPets() {
        console.log(this.pets);
    },

    bindButton() {
        const btn = document.querySelector("button");

        //podajemy nazwę funkcji, ale this w jej wnętrzu będzie wskazywać na button
        btn.addEventListener("click", this.showPets);

        //w tej wersji to samo
        btn.addEventListener("click", function() {
            this.showPets(); //błąd bo btn nie ma takiej metody
        });
    }
}

ob.bindButton();

Jeżeli chcielibyśmy by this wskazywał na obiekt czy klasę w której takie podpięcie występuje, musimy - jak powyżej - użyć funkcji bind(), lub arrow function (metodę z dodatkową zmienną zostawmy na kiedy indziej):


const ob = {
    pets : ["kot", "pies", "chomik"],

    showPets() {
        console.log(this.pets);
    },

    bindButton() {
        const btn = document.querySelector("button");

        btn.addEventListener("click", this.showPets.bind(this));
        //lub
        btn.addEventListener("click", () => this.showPets());
    }
}

ob.bindButton();

I wszystko by było super gdybyśmy w przyszłości nie zapragnęli wyłączyć obsługi kliknięcia dla naszego przycisku. Jeżeli chcemy odpiąć nasłuchiwanie zdarzenia, zawsze musimy podawać referencję do funkcji którą właśnie podpinaliśmy (czyli podać nazwę wcześniej utworzonej funkcji).

W powyższym kodzie w żadnym z dwóch przypadków takiej referencji nie mamy, ponieważ zarówno pierwszy jak i drugi sposób tworzy nam zupełnie nowe funkcje.


//bind zwraca nam nową funkcję ze zmienionym this
btn.addEventListener("click", this.showPets.bind(this));

//funkcja strzałkowa w której odpalam funkcję showPets()
btn.addEventListener("click", () => this.showPets());

Jak sobie poradzić w takim przypadku? Można zastosować podejście, stosowane w klasowych komponentach Reacta, które polega na wcześniejszym stworzeniu odpowiedniego wiązania:


const ob = {
    pets : ["kot", "pies", "chomik"],

    showPets() {
        console.log(this.pets);
    },

    bindButton() {
        this.showPets = this.showPets.bind(this);

        const btn = document.querySelector("button");
        btn.addEventListener("click", this.showPets);
    }
}

ob.bindButton();

Przy czym podobne wiązania najczęściej tworzone są od samego początku, czyli w konstruktorze klasy:


class MyObj {
    constructor() {
        this.pets = ["kot", "pies", "chomik"];
        this.showPets = this.showPets.bind(this);
        this.bindButton();
    }

    showPets() {
        console.log(this.pets);
    }

    bindButton() {
        const btn = document.querySelector("button");
        btn.addEventListener("click", this.showPets);
    }
}

const ob = new MyObj();

call() i apply()

Żeby ten kurs traktować jako kompletny, nie wypada nie wspomnieć o call() i apply(). W codziennym programowaniu raczej nie za często będziesz z nich korzystał (chyba, że będziesz używał kopiowanego kodu ze Stackoverflow), dlatego nie bierz sobie do serca, jeżeli nie będziesz ekspertem w użyciu tych metod.

Javascript udostępnia dla funkcji dwie dodatkowe metody call(this*, param1*, param2*...) i apply(this*, [param1, param2]*), które służą do wywoływania danej funkcji, z możliwością przekazania this w pierwszym parametrze.


//mamy prosty obiekt, który ma jedną metodę
const ob = {
    name: "Marcin",

    show() {
        console.log("Mam na imię " + this.name);
    }
}

ob.show(); //Mam na imię Marcin

const ob2 = {
    name: "Roman"
}

//wywołuję metodę z pierwszego obiektu, ale ustawiam jej this na ob2
ob.show.call(ob2); //Mam na imię Roman

//jeszcze raz ją wywołuję, ale tym razem przekazuję jeszcze inny obiekt
ob.show.call({name : "Patryk"}); //Mam na imię Patryk

Obie funkcje więc idealnie nadają się do "zapożyczania" metod z innych obiektów.

Jeżeli dana funkcja wymaga jakiś wartości, możemy je podać jako kolejne parametry metody obydwu metod. Różnicą jest tutaj to, że dla call() podajemy je po przecinku, natomiast dla apply() w formie tablicy:


const ob = {
    name : "nikt",
    print(pet1, pet2) {
        console.log(`Nazywam się ${this.name} i lubię: ${pet1} i ${pet2}`);
    }
}

const user = {
    name : "Marcin"
}
ob.print.call(user, "marchewka", "buraki"); //Nazywam się Marcin i lubię: marchewka i buraki

const user2 = {
    name : "Karol"
}
const vegetables = ["ogórki", "pomidory"]
ob.print.apply(user2, vegetables); //Nazywam się Karol i lubię: ogórki i pomidory
ob.print.call(user2, ...vegetables); //ale możemy też rozbić

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem.
Aha - i ta strona korzysta z ciasteczek.