Haos u oblasti (ne)definisanosti

Česta greška je da ljudi smatraju da je Javaskript interpreterski jezik, što nije slučaj. Javaskript se ustvari kompajluje. Pre samog izvršavanja izvorni kod se šalje kompajleru gde se izvršavaju različiti procesi. Tokom kompajliranja obavlja se proces leksiranja(tokenizacije) što znači da se definiše oblast važenja promenjivih. To znači da je leksička oblast važenja ustvari oblast važenja koja se određuje prilikom kompajliranja.

Kako bismo razumeli proces leksiranja, najbolje je da postavimo primer. Ako posmatramo sledeći izraz var m = 17, na prvi pogled on izgleda kao jedan izraz ali ovaj proces može da se podeli u dva koraka:

  1. var m Deklaracija promenjive unutar oblasti definisanosti
  2. m = 17 Dodela vrednosti promenjivoj, 17.

Tokom procesa komajliranja, tačnije u procesu leksiranja, kompajler će da odluči kako i gde su definisane sve promenjive. Ovo je onaj isti mehanizam koji izaziva podizanje (hosting) o kome sam pričao u funkcijama u Javaskriptu.

Vrlo je važno odvojiti deklaraciju od dodele vrednosti zato što se te dve stvari dešavaju u potpuno različitom trnutku u vremenu. Ako iskoristimo sledeći primer:

console.log(m)
var m = 17

Šta će ovo da ispiše? Postoje tri mogućnosti:

  1. Reference error
  2. Undefined
  3. 17

Tačan odgovor je undefined, upravo iz razloga što se definicija promenjive dešava tokom leksičke (kopajlerske) faze. Dok se dodela vrednosti dešava tokom dinamičke faze.

Kada smo razjasnili da Javaskript nije interpreterski jezik, već se kompajluje i kombinacija je statičkog i dinamičkog izvršavanja, možemo da pređemo na oblast definisanosti.

Oblast definisanosti (scope)

Javaskripta ima dosta problema što se tiče oblasti definisanosti, pa zato sam i odlučio da sve to opišem ovde, ali ono što je možda i najveća inicijalna greška Javaskripte je da je zamišljena da leksička oblast definisanosti bude nad funkcijom, iako većina jezika imaju oblast definisanosti nad blokom (blokom se smatraju { } zagrade). Na žalost, još veći problem je što uprkos inicijalne ideje da oblast definisanosti bude nad funkcijama, videćemo da to nije tačno i da je moguće prevariti taj mehanizam.

Oblast definisanosti nad funkcijama

Kao što sam već rekao, ideja je da oblast definisanosti bude nad funkcijma, a evo i par primera:

var m = 17 // Globalna oblast

function foo() {
    var ninja = "tajna" // Foo oblast
}

console.log(m) // 17
console.log(ninja) // ReferenceError: ninja is not defined

Ono što ovde vidimo je da je var m definisano u globalnoj oblasti, što znači da je dostupno bilo gde u kodu, dok je sa druge strane var ninja definisano unutar funkcije što znači da je oblast važenje te promenjive samo unutar te funkcije. Ovaj primer je "idealan" primer gde se sve lepo izvršava, ali hajde da vidimo kako izgleda tok izvršenja.

Razmišljaj kao kompajler

Recimo da imamo sledeći primer:

var m = 17
var bar = 'bam'

function foo(m) {
    var bam = 'hi'
    m = 9
    n = 'fun'
}

Šta će ovde da se desi? Prvo će se izvršiti korak leksike, gde će kompajler da prođe i nađe sve definicije novih promenjivih.

Kompajler:
Linija 1: Nova promenjiva m, definiši je na globalnom nivou.
Linija 2: Nova promenjiva bar, definiši je na globalnom nivou.
Linija 4: Nova funkcija foo, definiši je na globalnom nivou.
Linija 4: Nova promenjiva m, definiši je na nivou funkcije foo.
Linija 5: Nova promenjiva bam, definiši je na nivou funkcije foo.

Ovim je završena leksička faza i sve promenjive su definisane.

Zatim dolazi druga faza kompajliranja a to je dodela vrednosti.

Interpreter:
Linija 1: Global jel imaš m? Da. Dodeli mu ovu vrednost.
Linija 2: Global jel imaš bar? Da. Dodeli mu ovu vrednost.
Linija 5: Foo jel imaš bam? Da. Dodeli mu ovu vrednost.
Linija 6: Foo jel imaš m? Ne. Global jel imaš m? Da. Dodeli mu ovu vrednost.
Linija 7: Foo jel imaš n? Ne. Global jel imaš n? Ne... Šta onda?

Ovo je ono gde nastaje problem a to je da će global da kaže:

Nemam ni ja tu promenjivu, hajde da ti je napravim... Izvoli!

Šta se sada deslio? Došlo je do kršenja pravila oblasti važenja unutar funkcije, naša promenjiva n je postala globalna iako se nalazi unutar funkcije.

Ovo je lako rešivo tako što koristimo strict režim.

Ovo je prvi problem, gde vidimo da oblast važenja nad funkcijom više ne važi.

Ipak nije samo funkcija

Na početku sam rekao da je ideja bila da oblast važenja bude nad funkcijama. To se promenilo u verziji ES3 kad je uveden try catch blok.

Ono što mali broj ljudi zna je da catch ima svoju oblast važenja.


try {
  throw new Error()
} catch(e) {
    console.log(e) // Error
}

console.log(e)

Ako se vratimo na prethodnu priču, ovo će i biti logično, zato što catch gledamo kao funkciju, što znači da je parametar funkcije lokalna promenjiva u odnosu na funkciju.

Ja lično ovo ne smatram kao "varanje" oblasti važenja zato što, ako posmatramo na pretohdno opisan način, onda ovo ima smisla. Ali postoje načini da se prevari oblast važenja.

Prevara oblasti važenja

Postoje dva načina da se prevari oblast važenja u Javaskripti ali oba ova načina treba izbegavati i ne preporučujem da se ikad koriste.

Zlo - Evil

eval() - Funkcija koja je češće poznatija kao evil (zlo) i to iz više razloga, je funkcija koja izvršava string kao Javaskript kod. Često se koristi kao primer za pravljenje digitrona gde konstruišemo string od brojeva i operatora zatim to prosledimo evalu na izvršenje.

eval("2+3-1") // 4

Problem je što eval izvršava bilo šta što mu pošaljemo, eval("alert('cao')") - ovo će da izbaci alert. Često je eval bio problem za XSS. Toliko je loše da ima posvećenu sekciju na MDN-u, Do not ever use eval!

Kakve veze ima sa oblašću važenja? I te kako ima veze, zato što eval pravi svoju oblast važenja:

var m = 17

function foo(str) {
    eval(str) // NIKAD OVO
    console.log(m)
}

foo('var m = 42');

Verovatno može da se zaključi izlaz ovog, a to je 42... Ovo se dešava zato što eval menja leksičku oblast važenja i izgleda kao da je var m = 42 bilo tu tokom kompajlovanja programa. Ovde takođe postoji problem optimizacije zato što poziva interpreter i nema optimizacije na nivou kompajlera.

With

with ključna reč se retko viđa, ja je iskreno nikad nisam video u produkciji, a videćemo da je to dobro. Ali može da se nađe u golfjs takmičenjima ili 1 kB, 13 kB Javaskript takmičenjima.

with nam omogućava da skratimo kod tako što prosledimo objekat za koji može da se evaluira oblast važenja:

var obj = {
    a: 17,
    b: 42
}

obj.a = obj.a + obj.b
obj.b = obj.b - obj.a

// Ovo može kraće da se napiše bez `obj`-a.

with (obj) {
  a = a + b
  b = b - a
  c = 1
}

console.log(obj.c) // undefined

with se u ovom slučaju ponaša tako što pravi svoju leksičku oblast definisanosti. To znači kao i do sada, ako nešto kad naiđe na vrednost prvo pita lokalnu oblast, što je parametar koji smo prosledili obj, ako ne može tu da nađe onda ide dalje globalno. I tu se javlja isti problem kao kad ne koristimo ključnu reč var, napraviće se globalna promenjiva.

Znači da će c biti globalno, a ne deo obj-a kao što smo možda očekivali.

ES6 je promenila Javaskriptu

Dolaskom EcmaScript 2015 (ES6) dobili smo potpuno novu Javaskriptu, sa ozbiljnim promenama od kojih su dve nove ključne reči let i const.

let i const su ključne reči za deklaraciju promenjivih koji rešavaju probleme var-a. Dodavanjem let-a i const-a Javaskripta je promenila svoju inicijalnu ideju definisanja oblasti, pošto nove ključne reči imaju oblast važenja na nivou bloka. Takođe promenjive se više ne podižu (hojstuju), već ostaju gde su deklarisane.

for (let i = 0; i < 17; i++) {
    // Neki kod
}
console.log(i); // Greška

Ovim smo rešili većinu problema, ali takođe ove reči treba gledati i sa strane optimizacije.

Optimizacija u deklaraciji

Možda je na prvi pogled vrlo čudno kako zamenom var-a let-om možemo da dobijemo bilo kakve dobitke u pogledu performansi. Ali ako zađemo malo dublje u rad Javaskripte u pozadini, možemo da vidimo i zašto.

Iako želim da napišem poseban članak kako radi Javaskript u pozadini, evo ukratko pregleda. Skupljač smeća (garbage colletor) je zadužen da se brine o memoriji, pošto ako stalno pravimo objekte i nikad ih ne brišemo kad tad će naša aplikacija da ostane bez memorije. Zato skupljač smeća kada "vidi" da se promenjiva više ne koristi on će da ukloni tu promenjivu i ono na šta ona pokazuje.

Pošto  var ima samo oblast važenja nad funkciom skupljač smeća može da očisti te promenjive samo ako su u funkciji inače su uvek globalne. Zato let i const koji imaju oblast važenja u odnosu na blok i biće obrisani kad se zatvori blok, pružaju optimizaciju.

var uslov = true

if (uslov) {
    var korsnik1 = { //... }
} else {
    var korsnik2 = { //... }
}

U ovom primeru korisnik1 i korisnik2 se nalaze u globalnoj oblasti i neće biti obrisani sve dok se ne završi program, dok ako ovo napišemo koristeći let ili const:

let uslov = true

if (uslov) {
    let korsnik1 = { //... }
} // korisnik1 Može da se obriše
else {
    let korsnik2 = { //... }
} // korisnik2 Može da se obriše

Zaključak

Možda ovaj članak deluje kao dug, ali postoji dosta problema i rešenja što se tiče oblasti definisanosti u Javaskripti. Potrebno je dobro vladati i znati koja će promenjiva gde da se javi kako bismo izbegli greške.

Možda ova priča zvuči kao nepotrebna zato što koristeći use strict, let i const nama su rešeni svi problemi. Na žalost to nije tako i ovaj članak je uvod za jedan dosta važniji a to je Šta je this u Javaskripti?