JavaScript objekat this i poziv funkcije
Sa objektom this smo se već ranije sporadično susretali. Jednostavno, praktično ga je nemoguće izbeći, ma koliko se trudili. Najčešće nailazimo na njega kada kreiramo funkcije događaja (event handlere). I uvek je objašnjenje bilo - "this u ovom slučaju predstavlja element na koji je kliknuto". To u stvari predstavlja prilično dobar odgovor na pitanje "Šta je to this?", iako nepotpun.
Prosto rečeno - objekat this predstavlja kontekst u kome se izvršava funkcija.
Objekat this je malo "teži" za programere koji imaju puno iskustva u klasičnim objektno-orijentisanim jezicima. Tamo bi se ova referenca uvek odnosila na klasu/objekat kome metod pripada. JavaScript jednostavno nije takav jezik - this se ne definiše tokom "kompajliranja", već tokom izvršavanja. Još jedna jako bitna stvar: svaka funkcija ima svoj this!!!
Otkud sva ova zapetljancija? Ne zaboravite da na funkciju mogu biti napravljene reference koje je potpuno "izvlače" iz okruženja u kome je funkcija originalno napravljena. Konkretno, svaka funkcija može "iz čista mira" biti proglašena za event handler nekog sasvim desetog elementa u web dokumentu. JavaScript mora da se izbori sa ovim stvarima i tako dolazimo do toga da u pet poziva funkcije, this može biti pet različitih objekata.
Na nama, kao programerima je da kontrolišemo kako će funkcija biti pozivana i da predvidimo šta će this predstavljati u funkciji. Prvo moramo da naučimo kako se postavlja this zavisno od načina pozivanja funkcije.
Načini pozivanja funkcije u JavaScriptu
Treba da znamo da je funkciju moguće pozvati na više načina:
- kao običnu funkciju
- kao metod objekta
- kao konstruktor
- preko call() i apply() metoda
Svaki od ovih načina u stvari definiše drugačiji mehanizam određivanja, šta će biti objekat this.
Običan poziv funkcije
Ok, ovo je bar lako. Još u početnim lekcijama smo naučili kako se poziva funkcija.
function fun() {}
ili
var fun = function() {}
...
fun();
Svaka funkcija koju pozivamo na ovaj način suštinski nema this, ili da budemo precizni, u ovako pozvanoj funkciji, objekat this je window. Znači, this postoji, i predstavlja referencu na "vrhunski" objekat u web programiranju, a to je uvek objekat window.
Funkcija kao metod objekta
Dakle, ako ćemo baš-baš po pravilu, objekti u JavaScriptu nemaju zaista "prave" metode, već samo svojstva koja su reference na funkcije. Naravno, funkciju je sasvim legitimno i normalno pozivati preko reference:
obj.metod = function() {}
ili
obj.metod = function fun() {}
...
obj.metod();
Kada funkciju pozivamo na ovaj način, this je objekat iz koga je funkcija pozvana. Znači, u funkciji se kreira lokalna promenljiva this koja predstavlja referencu na objekat čiji je ta funkcija metod.
Ako jednu istu funkciju proglasimo za metod različitih objekata, this će se razlikovati zavisno od toga iz kog objekta je pozvana.
Ovo je veoma čest način pozivanja funkcije, i funkcije događaja se uvek tako pozivaju. Tako da, kada želimo da radimo sa npr. "elementom na koji je kliknuto", u event handler funkciji prosto koristimo this. Koji god elemenat web dokumenta da je "primio" događaj, objekat this će referencirati baš taj element u funkciji.
Poziv funkcije kao konstruktora
Programeri vole da se drže svojih navika. Tako prevrću nebo i zemlju i dovijaju se na različite načine da dokažu kako se u JavaScriptu može programirati i objektno-orijentisano. Jedan od osnovnih koncepata OOP-a je kreiranje objekta na osnovu klase. Nešto slično može da se izvede i ovde, korišćenjem operatora new koji primenjujemo na funkciju koja "glumi" klasu i u stvari efektivno predstavlja konstruktor objekta.
function Klasa() {}
...
var obj = new Klasa();
Šta se ovde dešava? Operator new kreira novi objekat i izvršava konstruktorsku funkciju u kontekstu tog objekta (kao da je pozvana iz njega). Tako će objekat this u funkciji konstruktora referencirati taj novi objekat. Na kraju, promenljiva obj postaje referenca na taj novoformirani objekat.
Ovim stvarima ćemo se više baviti kada budemo obrađivali tehnike objektno-orijentisanog programiranja u JavaScriptu.
Poziv funkcije preko njenih metoda
Ok, kao da nam nije dosta problema za jedan dan... Funkcija ima svoje metode? O, da - funkcija je objekat u JavaScriptu. To znači da svakako može imati svoja svojstva i metode. Upravo dva takva metoda call() i apply() služe za poziv same funkcije, ali oni rade još jednu jako bitnu stvar - omogućavaju programeru da eksplicitno veže objekat this za funkciju koja se poziva.
Metod call() uvek ima jedan parametar više od same funkcije. To je zato što kao prvi parametar predstavlja objekat koji se prosleđuje funkciji kao this.
function fun(p1, p2, p3...) {}
...
fun.call(obj, v1,v2,v3...);
Metod apply() radi to isto ali malo drugačijim stilom. Ima samo dva parametra - prvi parametar je objekat koji će u funkciji predstavljati this, a drugi parametar je niz koji sadrži vrednosti za parametre funkcije.
function fun(p1, p2, p3...) {}
...
fun.apply(obj, [v1,v2,v3...]);
Primeri za pozive funkcija
Poziv metoda
Prvo, pogledajmo primer u kome pozivamo funkciju na "klasičan" način, ali i kao metod objekta i event handler.
function fun()
{
console.log(this.id ? this.id : "Nepoznat ID");
}
// kreiramo objekte
var prvi = {id:"PRVI"};
var drugi = {id:"DRUGI"};
// u dva različita objekta, dva različita metoda su u stvari ista funkcija
prvi.metod = fun;
drugi.poziv = fun;
// pozivamo funkciju
prvi.metod(); // rezultat je "PRVI" (this = prvi)
drugi.poziv(); // rezultat je "DRUGI" (this = drugi)
fun(); // rezultat je "Nepoznat ID" (this = window)
// element "blok" dobija fun kao event handler za klik mišem
document.getElementById("blok").onclick = fun; // kad kliknemo mišem, rezultat će biti "blok"
Da vidimo šta smo uradili. Prvo smo deklarisali funkciju. Ona svakako ima objekat this, ali moramo da proverimo da li taj objekat ima svojstvo id. Ako ima, ispisaće se, ako ne, dobićemo poruku o tome.
Zatim kreiramo dva objekta - prvi i drugi i pri tom im već definišemo id svojstva kao stringove u kojima se nalaze njihovi nazivi.
Onda dodajemo još dva svojstva metod i poziv koji predstavljaju reference na funkciju fun(). Efektivno, ovo su sada metodi objekata.
Konačno možemo da vidimo kako ovo radi - pozivamo metode objekata. Ne zaboravite da se u oba slučaja u stvari poziva funkcija fun(). Međutim rezultati će biti drugačiji, pošto se prvi put funkcija poziva kao metod objekta prvi (znači tada je this u funkciji u stvari prvi), a drugi put se poziva kao metod objekta drugi (tada je this u funkciji referenca na objekat drugi). Odatle i drugačiji rezultati.
Treći put pozivamo funkciju na običan način. Objekat this je u funkciji onda referenca na "vrhovni" objekat, odnosno window, a on nema svojstvo id.
Na kraju, recimo da u HTML-u imamo neki element, recimo <div id="blok">...</div>. Tom elementu dodajemo event handler - referencu onclick usmeravamo na fun(). Znači da će se funkcija izvršiti još i kada korisnik klikne na DIV blok. Tada ćemo dobiti još jedan rezultat - "blok", pošto će objekat this biti referenca na element na koji je kliknuto.
Closure zamka
Stvarno? Opet? Ove closure funkcije kao da više stvaraju nego što rešavaju problema... Pogledajmo šta može da nam se desi ako se zaboravimo.
function obrada()
{
var blok = document.createElement("div");
blok.innerHTML = "Ovo je DIV blok.";
blok.onclick = function() {
this.style.backgroundColor = "red";
window.setTimeout(function() {
this.style.backgroundColor = "";
}, 1000);
};
document.body.appendChild(blok);
}
obrada();
Unutar funkcije obrada() kreiramo DIV blok sa tekstom kome definišemo event handler za klik mišem. Kada se klikne mišem na taj DIV blok, zadaje mu se promena boje u crvenu i to tako što koristimo objekat this koji predstavlja sam taj DIV blok.
E, sad, ne bismo želeli da blok zadrži tu crvenu boju, pa pravimo tajmer koji posle jedne sekunde treba da vrati boju bloka na šta god da je bila. Sve to lepo namestimo, zadamo callback funkciju i u njoj, sasvim pogrešno pokušavamo da vratimo boju pozadine za objekat this. Greška je u tome, što ovaj this u callback funkciji tajmera nije isti kao this iz nadređene funkcije (event handlera). Ovde će se this odnositi na objekat window, pošto je "okidanje" tajmera suštinski njegov događaj.
Da bi ovo ispravno radilo, u callback funkciji bismo morali da se pozovemo na objekat blok, koji srećom (zahvaljujući tome što je ovo ipak closure funkcija) takođe predstavlja DIV blok.
...
window.setTimeout(function() {
blok.style.backgroundColor = "";
}, 1000);
...
Zezanje sa this objektom početnicima često bude stvar probe-i-greške, ali bitno je da za početak bar znate gde da tu grešku tražite.
- C. Lindley (2013): JavaScript Enlightenment, O’Reilly, Sebastopol