„this” w JavaScript

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

1

Widzę, że spisujesz treść szkolenia dla potomnych :)

Taka uwaga jakby się ktoś głowił – odpalanie niektórych z tych przykładów w Firebugu nic nie da, bo chyba nie do końca są one odpalane w globalnym scopie (this === window -> false).

Czy przedostatni przykład (ten z obj.foo();//window) mam rozumieć tak, że dlatego dostajemy obiekt globalny, bo funkcja b jest zadeklarowana w kontekście funkcji fn (a nie konstruktora), więc według wspomnianej zasady jako funkcja zadeklarowana w funkcji wskazuje na obiekt globalny?

Całkiem przez przypadek zastanawiałem się dzisiaj (jeszcze nim opublikowałeś ten artykuł ;) czy da się mając object literal w postaci:
var obj = { foo: 2, bar: ‚dwarazyfoo’ }
zadeklarować go tak żeby bar było np. dwa razy foo. Tzn jak się dobrać do jakiejś właściwości podczas deklarowania obiektu w ten sposób? Nie mówię tu o tym, że później mogę sobie zrobić obj.bar = 2*obj.foo.

2

Bardzo ładny tekst :).

W zagadce dostaniemy true. Zmienna res utworzona jak w przykładzie jest właściwością obiektu window:
res === window[‚res’]

Zatem odwołanie this.e wykona się w kontekście obiektu globalnego window. W tym kontekście:
this.e === window[‚e’]

Zmienna e (lub dokładniej: właściwość e obiektu window) ma wartość true, bo:
!3 === false
!!3 === true
Za sprawą operatora ! trójka zostaje zrzutowana do typu boolowskiego. Ponieważ jest różna od zera, po zrzutowaniu daje true. A true po podwójnym zaprzeczeniu to nadal true.

Jako bonus możemy się pobawić i wywołać funkcję fn w kontekście zwracanego przez nią samą obiektu :). Wtedy foobar zachowa się inaczej, bo this będzie wskazywało na zwracany obiekt (pierwszy parametr apply), a nie obiekt globalny window:

fn.apply(fn())[‚foobar’] === false;

Co wygrałem? :)

3

[…] źródło: ferrante.pl Follow us on Twitter 25 śledzących RSS Feed 238 czytelników “this” w JavaScript 1 głosuj! O "this" w JavaScript słów kilka.    więcej » […]

4

Piotrek: Dobrze kombinujesz. Co do ostatniego pytania to niestety nie przypominam sobie żadnego rozwiązania z this…

Krzysiek: Dostajesz talon na częstsze pisanie postów na blogu. Gratulacje!

Pozdrawiam

5

Hmm… W argumentacji Krzyśka, choć generalnie poprawnej jest chyba drobna luka, czyż nie? Chodzi mi o ten fragment: „Zmienna res utworzona jak w przykładzie jest właściwością obiektu window” – przecież to nie ma żadnego znaczenia właściwością czego jest ‚res’ – istotne jest tylko w jakim kontekście wykonamy funkcję ‚fn’ (a o tym Krzysiek nie wspomina). Gdyby zamiast „Zmienna res” wstawić w tamtym zdaniu „Funkcja fn” (i pewnie taka była nawet intencja autora), to moim zdaniem było by już w 100% poprawnie.

6

Racja, aczkolwiek mysle, ze to bylo celowe uproszczenie Krzysztofa.

Pozdr

7

this === window -> false pod Firebug rzeczywiście daje false. David de Rosier (też prowadzi szkolenia dla rekrutacyjny.pl) wytłumaczył mi to tak, że Firebug „przykrywa” globalne this. Rozwiązanie: zamiast this napisać self:

self === window //true

OlgaGr
8

>> Całkiem przez przypadek zastanawiałem się dzisiaj (jeszcze nim opublikowałeś ten artykuł ;) czy da się mając object literal w postaci:
>> var obj = { foo: 2, bar: ‘dwarazyfoo’ }
>> zadeklarować go tak żeby bar było np. dwa razy foo. Tzn jak się dobrać do jakiejś właściwości podczas deklarowania obiektu w ten sposób? Nie mówię tu o tym, że później mogę sobie zrobić obj.bar = 2*obj.foo.

heh – fajnie, że i takie pytanie padło gdyż ja się właśnie uczę gdy ktoś pyta ;)… nie wiem czy to zadziała na IE ale w operze i mozilli działa – wystarczy wkleić w pasku adresu coby sprawdzić:

javascript:foo={get test(){ return 2*this.c; }, set test(val){this.c=val;},c:33};alert(foo.test);foo.test=44;alert(foo.test);void(0)

ogólnie to pytanie odniosłem do obiektu JSON – gdyż wystarczyło tylko zdefiniować getter i setter – ale jakoś metody to ja wolę by były zakończone „normalnie” czyli nawiasem – a co do definiowania getterów to nie jest to jedyny sposób….

poza tym tworząc obiekty najszybszą metodą jest zdefiniowanie niezbędnego minimum w prototypie do zmiennych prywatnych lub niezbędnego minimum obiektu statycznego a potem dołączenie reszty przez prototype…

jeszcze ciekawą kwestią jest użycie this z funkcjami typu setTimeout (wiadomo – nie można bezpośrednio używać this w niektórych przypadkach – ale w wielu można ;) ) – ale raz na forum.php zostałem zaskoczony gdy jakoś umyśliło mi się pisać jak może być deczko optymalniej – jakoś po przypisaniu this do zmiennej prywatnej w metodzie z setTimeout gdzie this występowało 3 razy jakoś to dziwnie się zaczęło zachowywać – nagle this „zgłupiał” mimo poprawnego zapisu, mimo iż działał a ja tylko poprawiłem setTimeoute (i użycie this w tej funkcji też zależy od kilku sytuacji) – jak się okazało podczas tej drobnej obtymalizacji połączonej z hermetyzacją kodu w danej metodzie trzeba było zamienić wszystkie this na zmienną prywatną będącą właśnie tym this – w problem bardzo się nie wgłębiałem i stwierdziłem, że błąd następuje przy kompilacji skryptu do samej pamięci przez interpreter – dokładniej to taka sytuacji pojawiła się w tym temacie:

http://forum.php.pl/index.php?showtopic=98464&hl=

zegarek84
9

klawiatura mi się przycięła ;p

proszę o zamianę w mojej odpowiedzi fragmentu:
>> poza tym tworząc obiekty najszybszą metodą jest zdefiniowanie niezbędnego minimum w prototypie do zmiennych prywatnych lub niezbędnego minimum obiektu statycznego a potem dołączenie reszty przez prototype…
___________
na:
poza tym tworząc obiekty najszybszą metodą jest zdefiniowanie niezbędnego minimum w obiekcie do potrzebnych zmiennych prywatnych lub niezbędnego minimum obiektu statycznego a potem dołączenie reszty przez prototype…

zegarek84
10

Niezwykłe spojrzenie na sprawę, każdy winien rozczytać oraz
zapoznać się z przedmiotem.

Dodaj komentarz

Dozwolone tagi: <blockquote>, <code>, <strong>