Obiekty - zaawansowany this

Na początku tego rozdziału poznaliśmy tworzenie własnych obiektów oraz słowo kluczowe this, które wskazuje na dany obiekt.

W większości przypadków tak właśnie jest, ale tak naprawdę this wskazuje na obiekt, który w danym momencie wywołał daną metodę, czyli znalazł się duż przed kropką podczas wywołania:


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

myObj.draw() //this w metodzie draw() wskazuje myObj

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

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

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

setTimeout(...) //this w setTimeout wskazuje na window, ponieważ tak naprawdę napisaliśmy window.setTimeout

Spójrzmy na kolejny przykład. Pojedynczą funkcję wskazujemy jako metodę obiektu car1 i car2. Jej kod się nie zmienia, ale this wskazuje na obiekt, który właśnie ją odpala:


function printDrive() {
    console.log(this.brand + " - właśnie jadę!");
}

const car1 = {
    brand : "Mercedes",
    drive : printDrive
}

const car2 = {
    brand : "BMW",
    drive : printDrive
}

car1.drive(); //Mercedes - właśnie jadę!
car2.drive(); //BMW - właśnie jadę!

Podobnie będzie się dziać w przypadku funkcji czasu:


window.setTimeout(function() {
    console.log(this);
});

window.setInterval(function() {
    console.log(this);
});

window.alert("lorem ipsum");
Powyższe metody zazwyczaj będziesz widział w skróconych formach z pominięciem window.. Niczego to nie zmienia, a jedynie skraca zapis.

Podobnie jest z innymi metodami, które non stop będziemy używać. Przykładowo jeżeli dane nasłuchiwanie eventu wykona dany element (znajdzie się przed kropką przy wywołaniu), to w jej wnętrzu this będzie wskazywać na ten obiekt/element:


const btn = document.querySelector('.btn');

btn.addEventListener('click', function() {
    console.log(this); //btn
});

Wiedząc to przechodzimy do trudniejszego przykładu:


const obj = {
    pets : ["kot", "pies", "chomik"],
    bindBtn : function() {
        //tworzymy dynamicznie przycisk i dodajemy go do body
        const button = document.createElement('button');
        button.innerText = 'Kliknij';
        button.type = 'button';
        document.body.appendChild(button);

        //this === obj
        console.log(this.pets); //["kot", "pies", "żona"]

        //dodajemy mu klikniecie
        button.addEventListener('click', function() {
            //this === button
            console.log(this.innerText); //Kliknij
            console.log(this.pets); //?????? - jak się odwołać do powyższej tablicy pets?
        });
    }
}

const obj = new MyObject();
obj.bindBtn();

Po wywołaniu metody bindBtn() tworzymy nowy guzik i podpinamy mu event click.
Po jego kliknięciu powinien on wypisać tablicę pets z obiektu MyObject. Jak jednak to zrobić, skoro instrukcja this wewnątrz eventu button wskazuje na niego samego (bo button znajduje się przed kropką addEventListener).

Jest na to kilka sposobów.

Dodatkowa zmienna wskazująca na this

Pierwszym z nich (najstarszym) 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 = {
    name : "Marcin",
    printDelay : function() {
        const self = this;

        setTimeout(function() {
            console.log(this); //window
            console.log(self.name); //Marcin
        }, 2000);
    }
}

ob.printDelay();

const obj = {
    pets : ["kot", "pies", "chomik"],
    bindBtn : function() {
        const that = this;

        const button = document.createElement('button');
        button.innerText = 'Kliknij';
        button.type = 'button';
        document.body.appendChild(this.button);

        button.addEventListener('click', function() {
            console.log(this.innerText); //Kliknij
            console.log(that.pets);
        });
    }
}

obj.bindBtn();

Sposób nie zawsze bezpieczny do użycia. W dziale obiekty - dziedziczenie omawialiśmy metodę call i apply, które służą do zmiany this w danej funkcji. Jeżeli w powyższej metodzie zmienilibyśmy this, to równocześnie zmieniła by się też zmienna self.


const obj = {
    pets : ["kot", "pies", "chomik"],
    bindBtn : function() {
        const that = this;

        const button = document.createElement('button');
        button.innerText = 'Kliknij';
        button.type = 'button';
        document.body.appendChild(this.button);

        button.addEventListener('click', function() {
            console.log(that.pets);
        });
    }
}

obj.bindBtn(); //po kliknięciu na button ["kot", "pies", "żona"]

const ob2 = {
    pets: "nie ma"
}
obj.prototype.bindBtn.call(ob2); //po kliknięciu na button "nie ma"

Przy małych skryptach raczej nie ma to znaczenia. Wszystko zależy od zaawansowania naszego kodu.

bind()

Drugim sposobem jest skorzystanie z instrukcji bind(newThis, *params), za pomocą której możemy przekazać nowy kontekst dla this, które jest w danej funkcji:


const ob = {
    name : "Marcin",
    printDelay : function() {
        setTimeout(function() {
            console.log(this); //ob
            console.log(this.name); //Marcin
        }.bind(this), 2000);
    }
}

ob.printDelay();

const obj = {
    pets : ["kot", "pies", "chomik"],
    bindBtn : function() {
        const that = this;

        const button = document.createElement('button');
        button.innerText = 'Kliknij';
        button.type = 'button';
        document.body.appendChild(this.button);

        button.addEventListener('click', function() {
            console.log(this.pets);
        }.bind(this));
    }
}

obj.bindBtn();

Rozważmy zmianę this na jeszcze jednym przykładzie:


const ob = {
    name : "Marcin",
    print : function() {
        console.log(this.name);
    }
}

ob.print(); //Marcin

document.querySelector('#btn').addEventListener('click', ob.print); //co pokaże ob.print()?

Po kliknięciu w przycisk #btn this w wywołanej funkcji wskaże na ten przycisk, który nie ma funkcji print. Znowu - musimy zmienić this na właściwy obiekt:


const ob = {
    name : "Marcin",
    print : function() {
        console.log(this.name);
    }
}

ob.print(); //Marcin

document.querySelector('#btn').addEventListener('click', ob.print.bind(ob));

Poza wskazaniem nowego this funkcja bind pozwala także przekazać parametry do danej funkcji.


function showSomething(data) {
    alert(data);
}

const element = document.querySelector('.show-text');
element.addEventListener('click', showSomething.bind(element, "Ala ma kota"));


const ob = {
    users : [
        {name : "Marcin", age : 10},
        {name : "Piotrek", age : 15},
        ...
    ],

    printUserDetail : function(id) {
        console.log(this.users[id]);
    },

    bindButtons : function() {
        const buttons = document.querySelectorAll('.show-detail');
        for (const btn of buttons) {
            const id = btn.dataset.id; //pobieram atrybut data-id
            btn.addEventListener("click", this.printUserDetail.bind(this, id));
        }
    }
}

ob.bindButtons();

Funkcja strzałkowa

W wersji ES6 istnieje jeszcze jedno rozwiązanie problemu związanego z this.
Zwie się ono funkcją strzałkową, która poza krótszym zapisem, nie zmienia kontekstu this.
Jeżeli więc w powyższym kodzie podepniemy zdarzenie za pomocą funkcji strzałkowej, we wnętrzu takiego zdarzenia this się nie zmieni:


...

    //poniżej użyłem funkcji strzałkowej z ES6
    button.addEventListener('click', e => { //funkcja strzałkowa
        console.log( this.pets ); //this wskzuje już nie na button
    });

...

Funkcję strzałkową poznasz dokładniej w dziale omawiającym ES6.

Trening czyni mistrza

Poniżej zamieszczam kilka zadań, które w ramach ćwiczenia możesz wykonać:

  1. Stwórz obiekt który ma:

    - właściwość time - czas ok 2000
    - właściwość pets, która zawiera tablicę kilku zwierzaków
    - metodę print() - która po czasie "time" zrobi pętlę po tablicy pets i wypisze w konsoli każde zwierzątko dużymi literami
    Odpal metodę print()
    
                    const ob = {
                        time : 2000,
                        pets : ["pies", "kot", "świnka"],
                        print : function() {
                            setTimeout(function() {
                                for (const pet of this.pets) {
                                    console.log(pet.toUpperCase());
                                }
                            }.bind(this), this.time);
                        }
                    }
                    ob.print();
    
    
                    //lub dodatkowa zmienna
                    const ob = {
                        time : 2000,
                        pets : ["pies", "kot", "świnka"],
                        print : function() {
                            const self = this;
    
                            setTimeout(function() {
                                for (const pet of self.pets) {
                                    console.log(pet.toUpperCase());
                                }
                            }, this.time);
                        }
                    }
                    ob.print();
    
    
                    //lub ES6
                    const ob = {
                        time : 2000,
                        pets : ["pies", "kot", "świnka"],
                        print : function() {
                            setTimeout(() => {
                                for (const pet of this.pets) {
                                    console.log(pet.toUpperCase());
                                }
                            }, this.time);
                        }
                    }
                    ob.print();
                    
  2. Ściągnij stronę z tego adresu (prawy i zapisz jako). W kodzie strony masz skrypt tworzący konstruktor i na jego bazie jeden obiekt. Po kliknięciu na kolejne buttony powinny się w konsoli wypisać zwierzęta - dużymi, małymi i niezmienioną wielkością liter. Kod niestety nie działa prawidłowo. Napraw go.
    
                    //cała reszta kodu zostaje bez zmian
                    MyObj.prototype.bindBtn = function() {
                        document.querySelector('#button1').addEventListener("click", this.printBig.bind(this));
                        document.querySelector('#button2').addEventListener("click", this.printSmall.bind(this));
                        document.querySelector('#button3').addEventListener("click", this.printNormal.bind(this));
                    }