Niedawno przyznałem, że na podstawie szkolenia pojawią się nowe artykuły. Czasem będą one kopiować jego treść, a innym razem ją rozszerzą, czy dopowiedzą to, co zostało z różnych przyczyn pominięte/uproszczone. Czas na pierwszy temat, czyli co tak naprawdę oznacza mityczne słówko this w JavaScript.
this występuje w blokach funkcyjnych bądź w globalnym scope, jak zresztą zdążyliście już pewnie nieraz zauważyć. Ciężko tak naprawdę powiedzieć, co dokładnie oznacza. Chyba najlepszą definicją będzie to, że this wskazuje na jakiś obiekt w zależności od kontekstu, w jakim została użyta funkcja, w której go zdefiniowaliśmy. Pisząc kontekst mam nam myśli głównie sposób i miejsce wywołania.
Obiekt globalny
O JavaScript w przeglądarkach wiemy, że obiekt globalny wskazuje na window. Tym samym this użyty w globalnym kontekście będzie wskazywał także na window:
<script type="text/javascript">
window === this; // true
</script>
W ten sposób z łatwością możemy dojść do wniosku, że poniższa funkcja użyta w globalnym scope zawsze będzie zwracała obiekt globalny:
<script type="text/javascript">
var global = (function() { return this; })();
</script>
Konstruktory a funkcje
Zobaczmy, co dzieje się, kiedy zdefiniujemy jakąś funkcję, która dodaje pewną własność do obiektu, na który będzie wskazywał this:
var fn = function() {
this.foobar = "foo";
};
Mając do dyspozycji coś takiego, jak powyżej, możemy naciąć się na dwa zupełnie różne zachowania. Możemy przecież napisać potem tak:
var fn = function() {
this.foobar = "foo";
};
var obj = new fn;
Lub tak:
var fn = function() {
this.foobar = "foo";
};
fn();
W pierwszym przypadku tworzymy instancję nowego obiektu na podstawie konstruktora fn. Jak możemy się domyślić, w ten sposób this będzie wskazywał na nowoutworzony obiekt (obj) i będziemy mogli użyć czegoś podobnego:
var fn = function() {
this.foobar = "foo";
};
var obj = new fn;
obj.foobar; // "foo"
W drugim listingu nie korzystamy jednak z operatora new, co za tym idzie nie tworzymy bezpośrednio nowego obiektu na podstawie konstruktora. Uruchamiamy po prostu funkcję jak każdą inną. Po wykonaniu się tego kodu nie dostajemy błędu, tak że możemy być pewni, że wszystko poszło dobrze i jest on – przynajmniej pod względem składni – absolutnie poprawny. Co więc dzieje się tutaj?:
fn();
JavaScript w takich przypadkach domyślnie przekierowuje na samą górę scope i this będzie wskazywał na obiekt globalny. W ten sposób pod this będzie ukryta referencja do window i będziemy mogli użyć zmiennej globalnej foobar tak:
window.foobar; // "foo"
Bądź tak:
window['foobar']; // "foo"
A także tak, jak poniżej, jako że każdy składnik obiektu window jest zmienną globalną:
foobar; // "foo"
Warto także powiedzieć, że funkcje zdefiniowane w innych konstruktorach/funkcjach nie dziedziczą this z funkcji wyżej, tylko także odwołują się do globalnego obiektu:
var zewnetrzna = function() {
return function wewnetrzna() {
return this;
};
};
var fn = new zewnetrzna();
fn(); // window
Object literals
Ważny jest, jak powiedziałem, kontekst. Definiując sobie obiekt w postaci object literal, this odwołuje się domyślnie do obiektu, w którym został utworzony:
var obj = {
bar: function() { return this; }
};
Pisząc więc coś takiego:
obj.bar();
this wskaże na obj. Przy okazji udowodnijmy kolejny raz, że funkcje wewnętrzne nie dziedziczą this:
var obj = {
bar: function() {
console.log(this);
return function() {
console.log(this);
return this;
}
}
};
Szybki test potwierdza naszą tezę:
obj.bar()();
Choć pamiętajmy, że równie dobrze można zapisać tak:
var fn = obj.bar();
fn();
Wszystko wygląda tak dlatego, że JavaScript wspiera zwracanie funkcji, które możemy potem uruchomić dodając nawiasy wywołania.
Zobaczymy teraz bardzo ciekawą rzecz, związaną z kontekstem. Zdefiniujmy najpierw object literal:
var obj = {
bar: function() {
return this;
}
};
Uruchamiając to tak:
obj.bar(); // obj
this zwraca obj. Jeśli jednak literalnie zmienimy to, co będzie po kropce, kontekst będzie już inny:
var f = obj.bar;
f(); // window
Dlaczego tak?
Jako że zmienne globalne możemy pisać tak:
window['globalna'] = "cos";
window['globalna']; // "cos"
Równie dobrze możemy zrobić jak poniżej, co wyjaśni nam, dlaczego this wskazuje na window:
window['f'] = obj.bar;
window['f'](); // window
Mamy więc po lewej obiekt window, czyli funkcja f jest uruchamiana właśnie w jego kontekście i to do niego this będzie odnosił.
Przeanalizujcie też poniższy kod, który wynikł z inicjatywy Rafała Kukawskiego w trakcie rozmowy ze mną:
var fn = function() {
var b = function(){
return this;
};
this.foo = function() {
return b();
}
};
var obj = new fn();
obj.foo(); // window
Inne metody
this można również zmienić poprzez metody call i apply wywołane na konstruktorach, ale to temat na kolejny artykuł.
Na koniec ćwiczenie ze szkolenia – co zwróci następujący kod i dlaczego:
var e = !!3;
var fn = function() {
return {
foobar: this.e,
e: !!0
};
};
var res = fn();
res.foobar; // ?
Komentarze