Kontekst i 'this' w JavaScripcie
Słówko this
, mimo podobieństwa do C# czy Javy, w JavaScripcie działa trochę inaczej niż nam się może wydawać. Kontekst na jaki wskazuje this może być dowolnie zmieniany, a jego nieumiejętne użycie spowodowuje wystąpienie masy błędów w naszej aplikacji. Ustrzec przed nimi można się jedynie rozumiejąc do czego owe this służy i jak się z nim obchodzić. Zapraszam do lektury :)
Implicit binding
Mamy tutaj do czynienia z kontekstem zwykłych obiektów. Złota zasada w identyfikacji kontekstu to:
this to obiekt, który jest po lewej stronie kropki.
W myśl powyższej zasady możemy stwierdzić, że kontekstem funkcji meow()
będzie obiekt cat
i tak też się dzieje.
var cat = {
name: 'Filemon',
meow: function () {
console.log(this.name);
}
}
cat.meow();
// Filemon
Podobnie sprawa ma się w przypadku zagdnieżdżonych obiektów i ich funkcji. Najbliższy obiekt po lewej stronie (z reguły, ale o tym zaraz) jest kontekstem, w jakim zostaje wywołana dana funkcja:
var cat = {
name: 'Filemon',
brother: {
name: 'Mruczek',
meow: function () {
console.log(this.name)
}
},
meow: function () {
console.log(this.name);
}
}
cat.meow();
// Filemon
cat.brother.meow();
// Mruczek
Trzeba być bardzo ostrożnym jeśli chodzi o referencje do obiektów czy funkcji, gdyż takie przypisanie również powoduje zmianę kontekstu. Jest nim nadal cat (Filemon), a nie jakby można było sądzić brother (Mruczek).
cat.meow = cat.brother.meow;
cat.meow();
// Filemon
Istnieją jednak sposoby na całkowie przejęcie kontroli nad tym co ma być w danej chwili “bazą”.
Explicit binding
Oprócz operowania obiektami i zasadą “kropki” istnieją inne metody (pewne 3 funkcje), które pozwalają na zmianę kontekstu wywołania dowolnej funkcji.
call()
Pozwala na wywołanie funkcji z konkretnym kontekstem przekazanym jako argument.
var meow = function () {
console.log('I am a cat ' + this.name);
};
var filemon = {
name: 'Filemon'
};
var mruczek = {
name: 'Mruczek'
};
meow.call(filemon);
// I am a cat Filemon
meow.call(mruczek);
// I am a cat Mruczek
bind()
Jest to bardzo podobna funkcja do call()
z tą różnicą, że pozwala na “przechowanie” funkcji z nowym kontekstem w postaci zmiennej, aby móc ją na przykład przekazać dalej jako parametr funkcji.
var meow = function () {
console.log('I am a cat ' + this.name);
};
var filemon = {
name: 'Filemon'
};
var mruczek = {
name: 'Mruczek'
};
var filemonMeow = meow.bind(filemon);
filemonMeow();
// I am a cat Filemon
var mruczekMeow = meow.bind(mruczek);
mruczekMeow()
// I am a cat Mruczek
apply()
To taki helper składający tablicę w argumenty funkcji, który również jako parametr przyjmuje nowy obiekt, a ten stanie się this w tej właśnie funkcji.
var myCats = function (cat1, cat2) {
console.log('I am ' + this.name + ' and my cats are: ' + cat1 + ' and ' + cat2);
};
var me = {
name: 'Krzysztof'
};
var cats = ['Filemon', 'Mruczek'];
myCats.apply(me, cats);
// I am Krzysztof and my cats are: Filemon and Mruczek
New binding
Dochodzimy w końcu do momentu, z którym większość będzie najbardziej zaznajomiona, czyli konstruktory i keyword new
. Sytuacja jest tu o tyle prosta, że to new nadaje kontekst całego obiektu podczas jego tworzenia.
function Cat(name, color) {
this.name = name;
this.color = color;
this.sayHello = function () {
console.log('Hi, I am ' + name + ' colored ' + color);
};
}
var filemon = new Cat('Filemon', 'black');
filemon.sayHello();
// Hi, I am Filemon colored black
Zwróćcie uwagę na brak this w wywołaniu funkcji sayHello()
. W każdym wypadku jego użycie jest opcjonalne. Domyślnie interpreter zawsze będzie wywoływał funkcję w kontekście rozwiązanym zgodnie z zasadami, które tu dzisiaj opisałem. Dla czytelności jednak lepiej jest użyć kontekstu, aby czarno na biało było widać co z czego jest wywoływane.
Spójrzcie na przykład bardziej zawiły, gdzie gdyby nie osobna referencja do obiektu macierzystego, odwołanie się do jego własności byłoby niemożliwe.
function Cat(name, color) {
var self = this;
self.name = name;
self.color = color;
self.sayHello = function () {
console.log('Hi, I am ' + self.name + ' colored ' + self.color);
};
self.brother = {
name: 'Mruczek',
sayHello: function () {
console.log('Hi, I am ' + this.name + ' and my brother is ' + self.name);
}
};
}
var filemon = new Cat('Filemon', 'black');
filemon.sayHello();
// Hi, I am Filemon colored black
filemon.brother.sayHello()
// Hi, I am Mruczek and my brother is Filemon
window binding
Wyżej napisałem o tym, że pominięcie this spowoduje automatyczne “dopięcie” odpowiedniego kontekstu. Jednak gdy funkcja wywołana jest globalnie, to (przynajmniej w przeglądarkach) jej kontekstem będzie obiekt window
.
function openWindow() {
console.log(this);
};
openWindow();
// Window {external: Object, chrome: Object, document: ...}
Jednak jeśli popełnimy pewien błąd, którego konsekwencje zostały opatrzone stosownym błędem w konsoli w ECMAScript 5 (‘strict’ mode, w ECMAScript 3 this wskazywał na window, stąd to całe zamieszanie), polegający na wywołaniu konstruktora funkcji bez użycia new, to otrzymamy błąd.
function someConstructor() {
this.a = 'foo';
this.b = 'bar';
}
var good = new someConstructor();
var bad = someConstructor();
// "TypeError: this is undefined"
this czy nie this - o to jest pytanie!
Mam nadzieję, że szybki kurs z kontekstu w JS tutaj przedstawiony pomógł wam choć trochę nabrać pewności w używaniu this z głową w swoich aplikacjach. Jeśli macie jakieś pytania czy uwagi z chęcią na nie odpowiem, w komentarzach czy mail/twitter.