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 obA
//ani metodą obiektu obB
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 = {
showPets() {
pets : ["kot", "pies", "chomik"],
}
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 = {
showPets() {
pets : ["kot", "pies", "chomik"],
}
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 = {
showPets() {
pets : ["kot", "pies", "chomik"],
}
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();
Trening czyni mistrza
Jeżeli chcesz sobie potrenować zdobytą wiedzę z tego działu, to zadania znajdują się w repozytorium pod adresem: https://github.com/kartofelek007/zadania-obiekty
W repozytorium jest branch "solutions". Tam znajdziesz przykładowe rozwiązania.