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 : printDriveF
}

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

car1.drive(); //Mercedes - właśnie jadę!
car1.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 MyObject = function() {
    this.pets = ["kot", "pies", "żona"]
}
MyObject.prototype.bindBtn = function() {
    //tworzymy dynamicznie przycisk i dodajemy go do body
    const button = document.createElement('button');
    button.innerText = 'Kliknij';
    button.type = 'button';
    document.body.appendChild(this.button);

    //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?

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 MyObject = function() {
    this.button = null;
    this.pets = ["kot", "pies", "żona"]
}
MyObject.prototype.bindBtn = function() {
    const that = this;

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

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

const obj = new MyObject();
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 MyObject = function() {
    this.button = null;
    this.pets = ["kot", "pies", "żona"]
}
MyObject.prototype.bindBtn = function() {
    const that = this;

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

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

const obj = new MyObject();
obj.bindBtn(); //["kot", "pies", "żona"]

const ob2 = {
    pets: "nie ma"
}
MyObject.prototype.bindBtn.call(ob2); //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 MyObject = function() {
    this.button = null;
    this.pets = ["kot", "pies", "żona"]
}
MyObject.prototype.bindBtn = function() {
    const button = document.createElement('button');
    button.innerText = 'Kliknij';
    button.type = 'button';
    document.body.appendChild(this.button);

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

const obj = new MyObject();
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));
                    }