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, czy raczej 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 Object [global])


console.log(this); //window

W każdym momencie możemy odwołać się do tego globalnego kontekstu poprzez użycie zmiennej globalThis:


const ob = {
    show() {
        console.log(this); //ob
        console.log(globalThis); //window
        console.log(globalThis === window); //true
    }
}

ob.show();

Function context

Jeżeli this używany jest wewnątrz funkcji, jego wartość zależna jest od tego, jak dana funkcja została wywołana.

Jeżeli funkcja nie jest metodą żadnego obiektu, wtedy po jej odpaleniu 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 trybu strict mode lub używanie modułów ES6 które automatycznie włączają ten tryb. Jeżeli użyjemy tego trybu, wtedy odpalając kod 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 (czyli dany obiekt znajduje się tuż przed kropką), 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

Podobnie this będzie zachowywał się dla funkcji eventów, wewnątrz których wskazuje na element, dla którego dane nasłuchiwanie zostało dodane:


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

//podpinamy mu nasłuchiwanie kliknięcia
btn.addEventListener("click", function() {
    console.log(this); //btn
});

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


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

const ob = {
   name : "pies",

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

        //poniższa funkcja NIE JEST metodą obiektu ob
        //ani metodą poniższego obiektu obj
        function testY() {
            console.log(this); //window
        }
        testY();

        const obj = {
            show() {
                console.log(this); //obj
            }
        }
        obj.show();
   }
}

ob.show();

Zmieniający się this

Wiedząc te wszystkie rzeczy, przejdźmy do kolejnego przykładu.


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.addEventListener("click", function() {
            console.log(this); //button

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

ob.bindButton();

Mamy obiekt ob, który zawiera tablicę pets oraz metodę bindButton(). W jej wnętrzu pobieramy ze strony button i podpinamy mu zdarzenie. We wnętrzu funkcji odpalanej po kliknięciu, this wskazuje na button.

Po kliknięciu przycisku w konsoli chcemy wypisać tablicę pets z obiektu ob. Jak jednak to zrobić, skoro this wewnątrz funkcji nasłuchującej kliknięcie wskazuje na dany button?.

Jest na to kilka sposobów.

Dodatkowa zmienna wskazująca na 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.pets); //["kot", "pies", "chomik"]
        });
    }
}

ob.bindButton();

Sposób prosty w użyciu, przy czym trzeba mieć w tyle głowy, że nie zawsze bezpieczny. Jak zobaczysz w dziale konstruktor - dziedziczenie możemy w naszym kodzie korzystać z funkcji call() i apply(), które służą do wywoływania danych funkcji, ale też pozwalają zmienić w ich wnętrzu wiązanie this na inny obiekt. Jeżeli w powyższej metodzie zmienilibyśmy this, to równocześnie zmieniła by się też zmienna that.


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

    bindButton() {
        const that = this;

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

ob.bindButton();

const ob2 = {
    pets: "nie ma"
}
ob.bindButton.call(ob2); //"nie ma"

Przy czym nie traktuj tej metody jako zło wcielone. To taki klasyk używany przez wiele skryptów, w których najwyraźniej nie było potrzeby używania ani call() ani apply(). Wszystko zależy od sytuacji.

Funkcja bind()

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

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


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

myFn(); //window

const myNewFn = myFn.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(); //["kot", "pies", "chomik"],

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 ma wiązania this, co oznacza, że this zwyczajnie się nie zmienia.

Jeżeli więc w powyższym kodzie podepniemy zdarzenie za pomocą funkcji strzałkowej, we wnętrzu takiego zdarzenia this się nie zmieni:


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 btn nie ma takiej metody
        });

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

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

ob.bindButton();

Arrow function 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();

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.

Menu