Closure funkcije u JavaScriptu
Učili smo da, kada se završi funkcija, sve njene lokalne promenljive prestaju da postoje. Isto tako smo u tekstu o oblasti važenja zaključili da unutrašnja funkcija ima pristup promenljivama njene nadfunkcije.
I sve je to bilo sasvim "čisto" i jasno, da u JavaScriptu ne postoji mogućnost da se unutrašnja funkcija "izvuče" preko reference. Drugim rečima, postoji mogućnost da se unutrašnja funkcija pozove i kada se nadfunkcija završi. Zaista, da li je greška kada takva funkcija pokuša da koristi promenljive svoje nadfunkcije? Ne. Takva funkcija se zove closure.
Dok funkcija "živi", živo je i njeno okruženje! Closure funkcija ne mora nikako specijalno da se obeležava. To je prosto odlika JavaScripta koju ćemo često koristiti.
Closure funkcije su nam najpotrebnije kada koristimo callback mehanizam i kreiramo funkcije koje obrađuju događaje (event handleri). Pogledajte par primera.
Upotreba closure funkcija
Callback funkcija
function nad()
{
var broj = 0; // lokalna promenljiva nadfunkcije
function fun() // unutrašnja funkcija
{
broj++; // ima pristup promenljivoj svoje nadfunkcije
console.log("BROJ: " + broj); // ispis rezultata u konzoli
}
window.setTimeout(fun, 5000); // izvlačimo podfunkciju kao callback
fun(); // poziv podfunkcije - sasvim obična stvar
console.log("Nadfunkcija je završena!"); // ovde naglašavamo da je f-ja gotova
}
nad(); // ovde počinje - pozivom f-je nad()
Da vidimo o čemu se radi. Imamo nadfunkciju nad() u kojoj se dešava "svašta nešto". Najpre, definiše se lokalna promenljiva broj i deklariše se unutrašnja funkcija fun(). Ova funkcija povećava promenljivu broj i ispisuje je. Sve je to sasvim u redu i ima smisla kada malo niže pozovemo funkciju fun(), sve u sklopu izvršavanja funkcije nad(). U konzoli dobijamo rezultat BROJ: 1. Na kraju funkcije nad(), u konzoli ispisujemo poruku da je ta funkcija završena, čisto da ne bude nedoumica.
Pazite sad, između ostalih naredbi, postavili smo i tajmer i zadali funkciju fun() kao callback, koja se poziva posle 5 sekundi. To je dobrano posle završetka funkcije nad()! Na ovaj način smo "izvukli" funkciju fun() koja će se izvršiti još jednom, ali će biti pozvana kada sve što je postojalo u funkciji nad() ne bi trebalo više da postoji.
Međutim, zahvaljujući closure mehanizmu, to nije slučaj. Funkcija fun() će biti pozvana kao callback posle 5 sekundi i dobićemo novi rezultat BROJ: 2. Znači, da je preko funkcije fun() ostalo "živo" celo njeno okruženje, odnosno sve lokalne promenljive iz funkcije nad(). Ovo bi bio slučaj čak i da imamo više nivoa ugnježdenih funkcija.
Event handler
function dodaj()
{
var blok = document.createElement("div");
blok.innerHTML = "Kliknite na dugme.";
document.body.appendChild(blok);
var dugme = document.createElement("input");
dugme.type = "button";
dugme.value = "Dugme";
dugme.onclick = klik; // definišemo event handler
document.body.appendChild(dugme);
function klik()
{
blok.innerHTML = "Kliknuto!";
}
}
//...
dodaj();
Evo i jednog primera koji bi lako mogao da se koristi u praksi. Funkcija dodaj() kreira jedan DIV blok i jedno INPUT dugmence. Da bi se promenila poruka u bloku, potrebno je kliknuti na dugmence i zato mu zadajemo onclick svojstvo kao referencu na funkciju klik().
Dok korisnik klikne na dugmence, funkcija dodaj() je odavno završena. Međutim, funkcija klik() koja se tada poziva, "održava u životu" sve lokalne promenljive njene nadfunkcije - i blok i dugme (iako joj dugme nije potrebno). Zahvaljujući tome, promeniće tekst u DIV bloku.
Closure zamka
Treba da budemo svesni jedne jako važne činjenice: vrednost nad-promenljivih ne mora biti ista kao u trenutku kreiranja closure funkcije.
Jednom kada počnemo da koristimo closure funkcije, ceo taj mehanizam lako može da nas zavede i natera da zaboravimo da iako closure održava svoje okruženje "u životu", sve te promenljive "žive taj život onako kako one hoće". To znači da se njihova vrednost može menjati na osnovu naredbi nadfunkcije ili čak neke druge closure funkcije.
Primer closure zamke
function napravi(N)
{
var blok;
for (var i=1; i<=N; i++) {
blok = document.createElement("div");
blok.innerHTML = "Klikni me.";
blok.onclick = function() // anonimna funkcija je closure
{
blok.innerHTML = "Ovo je BLOK " + i; // rezultat neće biti ono što želimo
};
document.body.appendChild(blok);
}
}
//...
napravi(10); // pozivamo funkciju koja kreira 10 blokova
Ovde smo išli malo unapred - kao event handler nismo zadali ime već pripremljene funkcije, već anonimnu funkciju. To smo uradili samo zato što je ovo tipičan izgled koda u kome se pravi greška. Inače, istu grešku bismo napravili i da smo koristili "običnu" funkciju.
Šta bi program trebao da radi? Pa imamo funkciju napravi() koja kreira N DIV blokova. Na svaki blok je moguće kliknuti, pošto smo svakom definisali event handler za klik mišem. Ono što očekujemo je da kad se klikne na recimo 5. blok, taj blok promeni sadržaj i da piše "Ovo je BLOK 5".
To se neće desiti. Gde god da kliknemo, uvek će se promeniti samo poslednji blok i u njemu će pisati "Ovo je BLOK 11". Šta se kog đavola desilo?
Imamo 10 blokova. Imamo 10 closure funkcija koje se aktiviraju kada kliknemo na blok. Svaka od tih funkcija koristi promenljive blok i i iz svoje nadfunkcije. I sve je to u redu. Jedino što vrednost ovih promenljivih u trenutku IZVRŠENJA closure-a nije ista kao u trenutku KREIRANJA closure-a, kako smo mi naivno pretpostavili.
Funkcija napravi() se prvo završi. Ona dotera svoj for ciklus do kraja. Referenca blok izmeni deset različitih DIV blokova i na kraju pokazuje na poslednji. Promenljiva i projuri kroz ciklus i dogura do vrednosti 11 koju zadržava posle ciklusa! I svaka od deset closure funkcija, pristupa upravo tim, takvim promenljivama.
Kako da rešimo ovaj problem? Elegantan način je korišćenjem anonimne samopozivajuće funkcije u koju "umotavamo" deklaraciju closure funkcije kako bismo dodali još jedan "sloj" lokalizacije, gde onda sačuvamo trenutne vrednosti promenljivih. Ovu tehniku ćemo pokazati kada budemo obrađivali anonimne funkcije.
U ovom slučaju ćemo lako razrešiti pitanje oko reference blok, korišćenjem objekta this u funkciji događaja. Objektom this ćemo se kasnije više baviti, a ovde to znači "ono na šta je kliknuto". Kada smo rešili kako da pravilno pristupimo DIV bloku, za promenljivu i ćemo iskoristiti "seljačku" varijantu - lukavo ćemo zapamtiti njenu trenutnu vrednost u samom DIV bloku.
function napravi(N)
{
var blok;
for (var i=1; i<=N; i++) {
blok = document.createElement("div");
blok.innerHTML = "Klikni me.";
blok.broj = i; // pamtimo promenljivu i u samom bloku
blok.onclick = function()
{
this.innerHTML = "Ovo je BLOK " + this.broj; // ovo funkcioniše
};
document.body.appendChild(blok);
}
}
//...
napravi(10);
Što bi rekli - bitno da radi.
- J. Resig, B. Bibeault (2013): Secrets of the JavaScript Ninja, Manning Publications, New York
- A. Rauschmayer (2014): Speaking JavaScript, O’Reilly, Sebastopol