Č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:
var m
Deklaracija promenjive unutar oblasti definisanostim = 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:
- Reference error
- Undefined
- 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 promenjivam
, definiši je na globalnom nivou.
Linija 2: Nova promenjivabar
, definiši je na globalnom nivou.
Linija 4: Nova funkcijafoo
, definiši je na globalnom nivou.
Linija 4: Nova promenjivam
, definiši je na nivou funkcijefoo
.
Linija 5: Nova promenjivabam
, definiši je na nivou funkcijefoo
.
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 eval
u 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?