Definicija i karakteristike
Poznata je činjenica da kada se završi funkcija, sve njene lokalne promenljive pokupi garbage collector i one prestaju da postoje u memoriji. Medjutim to nije slučaj za funkciju koja unutar sebe sadrži closure funkciju.
Closure su funkcije koje imaju pristup promenjivima koje se nalaze u domenu druge funkcije. Ovo se najčešće postiže ugradjivanjem funkcije unutar druge funkcije (čime se definiše “scope chain”), nakon čega closure dobija sposobnost da zapamti referencu na promenjive iz parent domena.
Treba napomenuti da se promenjive spoljne funkcije ne brišu po izvršavanju same funkcije, već se čuvaju u memoriji da bi bile dostupne closure funkciji. Nakon izvršenja closure funkcije, zatvara se i spoljna funkcija (odatle i naziv “closure“). Dok god se closure ne izvrše, JavaScript će čuvati i potrebne promenjive iz domena drugih funkcija, stoga one zauzimaju više memorije nego obične funkcije. Preterano korišćenje closure može da dovede do povećenja potrošnje memorije. Najnoviji JavaScript engine uspevaju da delimično oslobode zarobljenu memoriju, ali je i dalje stoji preporuka da se bude pažljiv pri korišćenju closure.
U sklopu debuger-a ugradjenog u browseru, može da se vidi closure izraz i koja je vrednost pormenjive “zapamćena” (pogledaj sliku).
Primer standardne closure f-je
Funkcija b() je ugnježdena unutar funkcije a() i duž “scope chain” potražuje promenjivu iz funkcije a(), stoga je ona klasičan primer closure funkcije.
1 2 3 4 5 6 7 8 |
function a (){ var x = "Ovo je closure"; function b (){ console.log (x); } b(); } a(); Vraća: Ovo je closure |
Nije svaka ugnježdena funkcija i closure
Ukoliko unutrašnja funkcija ne koristi promenjivu iz spoljnih funkcija u kojim se nalazi (duž “scope chain”), to onda nije closure. Pogledaj naredni primer:
1 2 3 4 5 6 7 8 |
function a (){ var x = 5; function b (){ console.log ("Ovo je ugnjezdjena funkcija, ali nije closure!"); } b(); } a(); // Vraća: Ovo je ugnjezdjena funkcija, ali nije closure! |
Closure pamti promenjive iz spoljne funkcije čak iako je spoljna fukcija izvršena
Poznato je da se nakon korišćenja naredbe return prekida izvršenje funkcije i postojanje njenih promenjivih u privremenoj memoriji. Medjutim “Closure” pamti promenjive iz spoljne funkcije čak iako je vratila neku vrednost sa return.
1 2 3 4 5 6 7 8 9 |
function a () { var x = "Ovo je closure, čak i pošto je spoljna funkcija završena i vratila rezultat"; function b () { console.log(x); } return b; } var c = a(); //Dodeljujemo return funkciju promenjivoj "c" c(); // Vraća: Ovo je closure, čak i pošto je spoljna funkcija završena i vratila rezultat |
Closure “pamti” referencu na koju ukazuje promenjiva
Kada se kaže da “closure funkcija” ima pristup promenjivoj pod tim se misli da ima pristup odredjenom mestu u memoriji na koju ukazuje ta promenjiva tj. njenoj referenci u trenutku pozivanja “clousure funkcije“. Ukoliko se na tom mestu u memoriji tj. referenci menja vrednost, clousure će uvek uzeti najnoviju trenutnu vrednost. U narednom primeru se vidi kako unutrasnja funkcija “pamti” referencu i koristi trenutnu vrednost u memoriji.
1 2 3 4 5 6 7 8 9 10 11 |
function povecajBrojac () { var brojac = 0; return function () { return brojac++; }; } var izbroj = povecajBrojac(); izbroj(); // Vraća: 0 izbroj(); // Vraća: 1 izbroj(); // Vraća: 2 |
Redeklarisanje promenjive
Ako nakon definisanja clousure, promenjivoj koja je potrebna closure dodelimo neku drugu referencu (mesto u memoriji koje čuva neku vrednost), clousure će da koristi referencu koja je bila u opticaju u trenutku definisanja clousure. U sledećem primeru, funkcija sayHello() u trenutku definisanja “pamti” referencu na globalnu promenjivu “myName” (“Dragoljub”). Pa iako “myName” menja referencu koja sada sadrži vrednost “Marko”, closure i dalje koristi vrednost “Dragoljub”.
1 2 3 4 5 6 7 8 9 10 |
var myName = 'Dragoljub'; var pozdrav = function(name){ return function(){ console.log('Hello ' + name + '!'); } } var pozdravSaImenom = pozdrav(myName); myName = 'Marko'; pozdravSaImenom();//Vraća: Hello Dragoljub! |
Medjutim to ne znači da closure pamti prvu definisanu vrednost neke promenjive, već pamti definisanu vrednost promenjive u trenutku definisanja closure. Pa tako u sledećem primeru funkcija vraća “Marko”
1 2 3 4 5 6 7 8 9 10 11 |
var myName = 'Dragoljub'; var pozdrav = function(name){ return function(){ console.log('Hello ' + name + '!'); } } myName = 'Marko'; var pozdravSaImenom = pozdrav(myName); myName = 'Pera'; pozdravSaImenom();//Vraća: Hello Marko! |
NAPOMENA:
Kada se koristi ključna reč “new” za kreiranje objekta unutar neke spoljne funkcije, TO NE KREIRA CLOSURE i nova funkcija nema referencu na lokalnu promenjivu spoljne fukncije kao closure.
Primena closure funkcije
Rešavanje problema sa “this” kod callback funkcije u petlji
Callback funkcija u petlji daje neželjene rezultate jer se ne poziva odmah dok petlja vrti, već uvek “malo kasnije” kada je petlja već izvrtela i došla do poslednje vrednosti brojača. Stoga callback funkcija uvek koristi poslednju vrednost brojača. Ovaj problem se rešava tako što callback funkciju odmah pozovemo (stavimo je u IIFE) da bi ona “zgrabila” vrednost promenjive “i” u tom trenutku, dok će closusure odraditi svoj deo posla i zapamtiti koja je to bila vrednost. Na ovaj način pravimo za svaki prolaz kroz petlju novu closure funkciju, koja “pamti” drugačiju vrednost promenjive “i”.
Primer br.1 – callback funkcija setTimeout() metode
1 2 3 4 5 |
for (var i = 0; i < 5; i++){ setTimeout(function(){ console.log(i); }, 1000); } // Vraća: 5 5 5 5 5 |
Po startovanju petlje početna vrednost promenjive “i=0”, nakon čega se poziva funkcija setTimeout(). Funkcija setTimeout() će tek za 10s pozvati callback funkciju. Dok se čeka na pozivanje callback funkcije, petlja nastavlja da “vrti” i sada je “i=1”, odmah zatim se poziva se funkcija setTimeout() koja će za 10s opet pozvati callback funkciju, a dok se čeka na njeno izvršenje, petlja vrti dalje i sada je “i=2”. Postupak se ponavlja sve dok “i” ne dobije vrednost 5. Ovaj ceo prethodno pomenuti postupak je izvršen veoma kratkom periodu (mereno milisekundama).
Nakon prvih 10sec petlja već “izvrtela” promenjivu “i” i “poziva” se (invoke) prva callback funkcija koja stoga uzima vrednost promenjive “i” a to je 5. Naredna callback funkcija za se aktivira za dodatnih 10sec, i koristi istu promenjivu pa je rezultat je isti…
Rešenje problema:
U ovom primeru pozivamo funkciju pri svakom krugu petlje uz pomoć IIFE, i tako joj obezbedjujemo jedinstvene vrednosti za svaki loop.
1 2 3 4 5 6 7 |
for (var i = 0; i < 5; i++){ (function(i){ setTimeout(function(){ console.log(i); }, 1000); })(i); } // Vraća: 0 1 2 3 4 |
Primer br. 2 – callback funkcija addEventListener()
Na sledećem primeru se javlja sličan problem kao kod petlje i funkcije setTimeout():
See the Pen VWPNPq by Web programiranje (@chos) on CodePen.
Kao u svakom standardnoj petlji na svaki element je dodat eventListener i petlja se završila. U trenutku kad se klikne na neko dugme petlja je već “izvrtela” i koje god dugme da se izabere alert će da bude jednak.
Rešenje
Jedno od mogućih rešenja je da se stavi ceo kod u IIFE koja će se pozivati pri svakom krugu a clousure će da pamtiti prosledjene vrednost:
See the Pen gRgyOy by Web programiranje (@chos) on CodePen.
Imitacija privatnih podataka
JavaScript je jezik koji ne podržava privatne podatke u istom smislu kao neki drugi programski jezici, ali koristeći closure možemo da imitiramo ovakvo ponašanje. Sve se zasniva na osobini closure da ona jedina ima pristup promenjivoj u spoljnoj funkciji i nakon što je funkcija završena. Pošto se takvoj promenjivoj može pristupiti jedino iz closure funkcije, može se smatrati da je promenjiva privatna.
Konstruktorski šablon za kreiranje privatnih podataka
U narednom kodu je prikazana standardna konstruktorska funkcija sa svojstvom objekta “_name”.
1 2 3 4 5 6 7 |
function Person(name) { this.name = name; this.getName = function() { return this.name; }; } |
Promenjiva “name” nije privatna jer postoji mogućnost pristupa iz globalnog domena. Nakon kreiranja novog objekta je moguće promeniti vrednost promenjive sa jednostavnim dodeljivanjem vrednosti svojstvu objekta.
1 2 3 |
var person = new Person("Pera"); person.name = "Dragoljub"; alert(person.getName()); // Vraća novu vrednost promenjive "Dragoljub" |
Ako želimo da imitiramo dobru praksu objektnog programiranja iz drugih jezika i sprečimo pristup ovoj promenjivoj iz globalnog domena, koristićemo closure i definisaćemo lokalnu promenjivu umesto svojstva objekta.
1 2 3 4 5 6 7 |
function Person(name) { var _name = name; this.getName = function() { return _name; }; } |
Sada jedini način da se “zapamti” vrednosti promenjive pri stvaranju novog objekta, je preko closure getName(). I dalje postoji mogućnost da se pristupi svojstvu objekta i da se promeni vrednost, ali closure vraća vrednost promenjive pri konstruisanju objekta. Ovo je objašnjeno u delu redeklarisanje promenjive.
1 2 3 |
var person = new Person("Pera"); person._name = "Dragoljub"; alert(person.getName()); //Vraća zapamćenu vrednost promenjive u trenutku konstruisanja objekta "Pera" |
PRIVILEGOVANE METODE:
Privilegovanje metode su globalne metode koje imaju pristup privatnim promenjivama ili funkcijama.
U sledećem primeru kreiramo “privilegovane metode” koje mogu da menjaju privatne promenjive.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function Person(name){ var _name = name; this.getName = function(){ return _name; }; this.setName = function (value) { _name = value; }; } var person1 = new Person("Pera"); var person2 = new Person("Steva"); console.log(person1.getName()); //"Pera" person1.setName("Dragoljub"); console.log(person1.getName()); //"Dragoljub" console.log(person2.getName()); //"Steva" |
Mana ovog postupka je što je za primenu neophodno koristiti konstruktorski šablon koji pri svakom instanciranju novog objekta instanciraju i ove dve metode (getName i setName), ostale metode mogu to izbeći korišćenjem prototype patern-a.
NAPOMENA:
Najbolja implementacija privatnih svojstava u JS-u je konačno dodata u ES2022 sa hash # prefiksom.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class Person { #_name constructor(name) { this.#_name = name; } getName (){ return this.#_name; }; setName (value) { this.#_name = value; }; } var person1 = new Person("Pera"); console.log("person1: ", person1.getName()); //"Pera" // Promena privatnog svojstva sa privilegovanom metodom: person1.setName("Dragoljub"); console.log("person1 (posle promene imena): ", person1.getName()); //"Dragoljub" var person2 = new Person("Mirka"); console.log("person1: ", person1.getName()); //"Dragoljub" console.log("person2: ", person2.getName()); //"Mirka" //Pokušaj promene svojstva bez privilegovane metode bi vratio Error person1.#_name = "Steva" // Vraća Error |
ovo je dragocen materijal za sve one koji se trude da nauce vise o java scriptu. hvala i nastavite!
poslednja dva reda u komentarima kazu da je kreiranje persone broj 2 prepisalo podatke u prvom objektu?
alert(person1.getName()); //”Mirka”
alert(person2.getName()); //”Mirka”
Upravo tako…ubaci ceo kod u konzolu i dobićeš taj rezultat, sa ovim primerom sam hteo da pokažem kako se clousure može koristiti za kreiranje statičnog svojstva (svojstvo koje je dostupno svim instancama).
Pozdrav! Zasto je u poslednjem primjeru, name definisano van funckije Person. Ne razumem ovakvu upotrebu, jer svaki put kada kreiramo novi objekat, prethodna vrijednost varijable name ce biti pregazeno.
Vuče hvala na komentaru,
u tom delu sam hteo da objasnim “statična” svojstva, ali očigledno da tome nije mesto u ovome članku jer u ovome kontekstu privatnih promenjivih samo stvara zabunu, tako da sam taj deo izbacio ali i ažurirao privatna svojstva sa najnovijim JS pristupom.