Privatnost u Javaskripti, Closure

U prethodnom članku, "Haos u oblasti (ne)definisanosti", smo upoznali kako zaista funkcioniše oblast definisanosti u Javaskripti i koji su česti problemi. Zbog svih navedenih problema i ideje da se Javaskript približi ostalim programskim jezicima, javila se potreba za privatnošću u Javaskripti.

Šta je privatnost?

Ideja privatnosti je da samo određeni članovi iz određene oblasti mogu da pristupaju promenjivama, dok je pristup ostalim članovima van te oblasti zabranjen.

Nakon onoga što smo naučili, nije tako teško postići privatnost u Javaskripti, zar ne?

function foo() {
    var privatno = 'tajna';
    
    console.log(privatno);
}

foo(); // 'tajna'
privatno; // Reference error

Ovim smo efektivno sakrili promenjivu privatno, jer zbog oblasti važenja ova promenjiva samo postoji unutar funkcije foo.

Kraj? Ne bih rekao. Problem je u tome što ova promenjiva živi samo dok i funkcija živi i ako želimo da radimo još nešto sa njom to neće biti moguće.

Ako se malo prisetimo kako radi leksička oblast, a to je tako što pita na gore da li neko ima tu referencu, shvatamo da upravo taj mehanizam možemo iskoristiti.

function foo() {
    var privatno = 'tajna';
    
    function bar() {
        console.log(privatno);
    }
    
    bar();
}

foo(); // 'tajna'

Iz ovog primera vidimo da unutrašnja funkcija ima pristup članovima "iznad" nje. Ovo je odlično ali ne rešava naš problem zato što mi nemamo pristup već se ovo izvrši jednom i zatim obriše. Očigledno, neophodno nam je drugačije rešenje.

Closure

Rešenje za ovaj problem je Closure, vrlo česta reč u kombinaciji sa Javaskript-om i razgovorom za posao. Veliki broj ljudi smatraju da je ovo neophodno znati i često se forsira kao eliminaciono pitanje na razgovoru za posao. Videćemo da ovo uopšte nije strašno. Sada kad znamo kako radi oblast definisanosti, Closure je logična stvar.

Dakle, šta je closure? Funkcija koja vraća fukciju... Možda zvuči suviše prosto ali jeste upravo to.

function foo() {
    var privatno = 'tajna';
    
    function bar() {
        console.log(privatno);
    }
    
    return bar();
}

var bar = foo();
bar(); // tajna
bar(); // tajna

Ovim smo dobili primer pristup funkciji foo van nje. Kako ovo radi?

Kada mi pozovemo funkciju foo ona nama vrati referencu na funkciju bar. Samim tim što je vratila referencu na nešto unutar nje skupljač smeća (garbage collector) neće obrisati funkciju foo sve dok postoji referenca. Sve funkcije unutar funkcije foo znaju za njene promenjive, samim tim mogu i de pristupe njenom sadrzaju.

Petlje

Recimo da imamo ovaj kod:

for (var i = 0; i < 17; i++) {
    setTimeout(function () {
        console.log(i);
    }, 1000);
}

Ovaj kod sam iskoristio zato što se često javlja u projektima i predstavlja problem. Ovaj kod da loguje 17 puta broj 17. Čudno?

Ako pokrenemo linter nad ovim kodom, dobicemo no-loop-func error. Ovo je vrlo čest probelem i javlja se zbog nepoznavanja kako closure radi.

Ono što sigurno možemo da zaključimo je da se setTimeout pozvao posle petlje, to ima smisla. Zato što petlja ne čeka da se izvrši setTimeout već ide dalje. Šta više, sve setTimeout funkcije unutar sebe referenciraju na istu promenjivu, i u ovom slučaju je to stvarno referenca a ne kopija. Što znači, kad dođe vreme da se izvrši setTimeout to je posle petlje, a naša vrednost je već povećana.

Lako možemo da zaključimo da se ovo sve izvršava posle petlje tako što pogledamo krajnji broj. Nama je uslov i < 17, što znači da petlja treba da stane kad je broj i = 16. To jeste istina, ali kada petlja stane i = 17 zato što se i++ izvršilo u prethodnom koraku.

Ono što nama treba ješte da postoji kopija i u svakoj iteraciji, čime postižemo da je i jedinstveno i ne zavisi od petlje. Sad kad znamo šta je Closure i kako radi hajde da probamo to rešenje.

for (var i = 0; i < 17; i++) {
    (function() {
        setTimeout(function () {
            console.log(i);
        }, 1000);
    })()
}

Da skratim muke pokretanja ovog koda, ovo neće raditi. Vidimo da iako imamo leksičku oblast važenja pomoću IIFE ovo ne rešava problem. Neki već sad vide rešenje, a problem je u tome što je naša oblast prazna. Ona i dalje referencira istu promenjivu.

for (var i = 0; i < 17; i++) {
    (function() {
        var j = i;
        setTimeout(function () {
            console.log(j);
        }, 1000);
    })()
}

Napokon, ovo radi i dobijamo izlaz brojeva od 0 do 16. Ono što je bilo bitno je da prilikom svake iteracije mi unutar funkcije napravimo novu kopiju za promenjivu i. Ovaj trenutni kod možemo da poboljšamo tako što i prosledimo kao vrednost funkcije. Ovo će raditi zato što JS prosleđuje primitivne promenljive po vrednosti a ne po referenci.

for (var i = 0; i < 17; i++) {
    (function(j) {
        setTimeout(function () {
            console.log(j);
        }, 1000);
    })(i)
}

Elegantnije rešenje?

Iako je rešenje gore potpuno validno i tačno, moguće je značajno poboljšati kod. Ono što je bilo potrebno da bismo rešili problem je da za svaku iteraciju napravimo blok oblast važenja, što već znamo da uradimo zahvaljujući ES6 (živ bio i veliki porasooo!!!).

for (let i = 0; i < 17; i++) {
    setTimeout(function () {
        console.log(i);
    }, 1000);
}

Ako otvorimo specifikaciju i pogledamo definiciju

for ( LexicalDeclaration Expressionopt ; Expressionopt )

U drugom koraku kaže Let loopEnv be NewDeclarativeEnvironment(oldEnv)., što znači da za svaki korak u petlji se definiše nova leksička oblast koja je ulančana sa starom.

Pravljenje nove oblasti i ulančavanje sa starom je nekad bila vrlo skupa operacija. Ako pokrenemo dve petlje sa potpuno istim sadržajem a jedina razlika je da jedna koristi var a druga pre možemo da vidimo da danas daju jednake rezultate. Ali u ranijim implementacijama var petlja je bila preko dva puta brža.

UserAgent Petlja sa var Petlja sa let
Chrome 48.0.2564 5,891,754 ops/sec 11,683,983 ops/sec

Testovi su napisani i testirani na JSPerf. Takođe, ovaj bug je bio prijavljen Chrome timu.