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 też się odwołać do tego globalnego kontekstu poprzez użycie zmiennej globalThis:


const ob = {
    show() {
        console.log(this); //ob bo wskazuje na obiekt którego jest ta metoda
        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 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 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, 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 zdarzeń, 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 testA() {
    console.log(this); //window
}
testA();

const ob = {
   name : "pies",

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

        //poniższa funkcja mimo, że zagnieżdżona, NIE JEST metodą obiektu ob
        //ani metodą poniższego obiektu obj
        function testB() {
            console.log(this); //window
        }
        testB();

        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 btn
            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 się ono nie zmienia.

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 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()

Poniższe informacje traktuj raczej jako porządkowe, bo prawdopodobnie nigdy z nich nie skorzystasz (a przynajmniej w początkującym-średnio-zaawansowanym programowaniu).

Javascript udostępnia dla każdej 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.