Kontekst izvrsenja i leksicko okruzenje
Managing complexity is the most important technical topic in software development. Steve McConnell
Kod obicno razdjelimo u smislene cjeline kako bi kontrolisali slozenost pisanja koda uz pomoc funkcija, modula, objekata itd. Tako kontekst izvrsenja omogucava JavaScript enginu da kontrolise kompleksnost interpretiranja i izvrsavanja koda.
Kontekst izvrsenja
Novi kontekst izvrsenja (Execution Context) je kreiran kad god se skripta ili funkcija izvrsava, a koristi se za pracenje stanja izvrsenja funkcije. JavaScript kod se izvrsava kroz jedan od sledecih konteksta izvrsenja:
- Globalani kontekst
- Kontekst funkcije
- Eval - necemo diskutovati jer nije dovoljno relevantan
JavaScript engine (prevodilac ili interpreter) stvara novi kontekst kad god se sprema da izvrsi funkciju fn() {...}
ili skriptu koju smo napisali, a svaka skripta pocinje sa globalnim kontekstom izvrsenja. Svaki sledeci put kada pozovemo funkciju, JS engine kreira novi kontekst izvrsenja, stavlja kontekst funkcije na vrh pozivnog steka (execution stack) i izvrsava funkciju. Ako u sklopu izvrsenja funkcije (prije nego je ona zavrsila) pozovemo drugu funkciju, kreira se novi kontekst, dodaje se na vrh pozivnog steka i nova funkcija se izvrsava. Takav tok akcija se ponavlja dok se jedna funkcija ne izvrsi do kraja i njen kontekst se izbaci iz steka. Potom kontrolu preuzima kontekst izvrsenja koji je pozvao tu funkciju.
function forward() {
// code
}
let move = function() {
forward();
}
move();
Pozivni stack LIFO (Last In First Out) za kod iznad se moze predstaviti ovako:
↑ [ Kontekst forward() ] ↓ ↑ [ Kontekst move() ] ↓ ↑ [ Globalni kontekst ] ↓
Struktura konteksta izvrsenja
Kontekst izvrsenja konceptualno ima strukturu koja moze izgledati zbunjujuce ali ce njene komponente biti razradjene u tekstu. 🛑: Ispod navedeni objekti postoje teoretski da bi objasnili procese i ne mozemo direktno manipulisati njima.
var executionContext = { variableObject: {}, lexicalEnvironment: {}, this: {} }
Ono sto treba imati na umu je da svaki kreirani kontekst ima dvije faze: faza kreiranje i faza izvrsenja.
U fazi kreiranja izvrsnog konteksta
- Pravi se
variableObject
koji sluzi za pocetno skladistenje varijabli, argumenata i deklarisanih funkcija. - Pravi se
lexicalEnvironment
kao kopijavariableObject
u ovom trenutku. - Definise se vrijednost
this
i pokazuje na objekat na koji se funkcija primjenjuje u tom kontekstu.
U fazi izvrsenja izvrsnog konteksta
- Vrijednosti su dodijeljene
- Leksicko okruzenje se koristi za razrjesenje veza i identifikatora
Pocetno skladistenje i hoisting
Varijable i funkcije su predstavljene kao svojstva sa vrijednostima unutar objekta koji zastupa kontekst. U fazi kreiranja (prije izvrsenja koda) u tom objektu var
varijable imaju vrijednost undefined
, a svaka deklaracija funkcije unutar konteksta je svojstvo koje pokazuje na sadrzaj funkcije i ona se automatski pohranjuje u memoriju. Ovo znaci da su deklarisane funkcije dostupne i prije nego se skripta pocne izvrsavati, ali ako pristupimo var
varijabli prije nego je definisana za rezultat cemo dobiti undefined
. Ovaj proces pohranjivanja varijabli i deklarisanih funkcija (function declaration) se naziva hoisting.
Hoisting je JavaScript zadano ponasanje premjestanja deklaracija na vrh. Drugim rijecima, funkcije i neke varijable se mogu koristiti prije nego su deklarisane.
console.log(hello); // undefined
var hello = 'Hello. Pleasure to meet you.';
sayHi(); // Hello. Pleasure to meet you.
function sayHi() {
console.log('Hello. Pleasure to meet you.');
}
Kada se function expression pozove prije nego joj je dodijeljena vrijednost, tretira se kao varijabla i ima vrijednost undefined
. Kada je pozovemo prije deklarisanja dobijamo gresku da varijabla nije funkcija:
sayHi();
// Uncaught TypeError: sayHi is not a function
var sayHi = function() {
console.log('Hello. Pleasure to meet you.');
};
Ukoliko varijablu deklarisemo sa kljucnim rijecima let
ili const
hoisting nece raditi na isti nacin. Iako se varijabla podize na vrh bloka ona ima neinicijalizovano stanje i nalazi se u privremenoj mrtvoj zoni (temporal dead zone) — gotovo kao da varijabla ne postoji.
console.log(word);
// Uncaught ReferenceError:
// Cannot access 'word' before initialization
let word = 'Oui.';
console.log(nonExistingVariable);
// Uncaught ReferenceError:
// nonExistingVariable is not defined
Razlika izmedju nepostojece i neinicijalizovane varijable je u tome sto je JavaScript engine svjestan varijable ali je neupotrebljiva do njene deklaracije. Znamo da je svjestan varijable jer u kodu ispod dobijamo gresku da ne mozemo pristupiti varijabli name
prije inicijalizacije iako je vec jedna varijabla sa istim imenom definisana izvan funkcije.
let name = 'Mark';
function logName() {
console.log(name);
// ReferenceError:
// Cannot access 'name' before initialization
// Zona privremene neupotrebljivosti varijable je
// TDZ ili Temporal Dead Zone
let name = 'Ian'; //
}
logName();
Leksicko okruzenje
Svaka skripta, funkcija fn() {...}
i blok koda {...}
koji napisemo ima skriveni objekat variableObject
o kom smo maloprije govorili. Sadrzaj tog objekta se u fazi kreiranja kopira u lexicalEnvironment
objekat i predstavlja leksicko okruzenje. Ovaj objekat se sastoji od
environmentRecord
ili zapisanog stanja lokalnog okruzenja ukljucujuci argumente, deklaracije funkcija i varijable. Ovaj objekat cuva zapisana svojstva u kojem je funkcija kreirana bez obzira gdje je pozvana. Da bi azurirali varijablu, neophodno je promijeniti svojstvo leksickog okruzenja u kom zivi.- Referencu na vanjsko leksicko okruzenje, odnosno roditeljski blok koda. Globalni kontekst nema roditeljski opseg i u tom slucaju je vrijednost reference
null
.
Zato funkcija definisana unutar druge funkcije ima pristup roditeljskim varijablama, ne i obrnuto. Ovo ponasanje se naziva leksicki opseg (lexical scoping) koji je kljucan za zatvorenja (closures).
var phrase = 'Pleasure to meet you';
function sayHi() {
var name = 'Summer';
console.log(phrase + ' ' + name + '.');
}
sayHi(); // Pleasure to meet you Summer.
// Globalno leksicko okruzenje u fazi izvrsenja
globalLEnvironment = {
environmentRecord: {
phrase: 'Pleasure to meet you',
sayHi: fn() {...}
},
outer: null // nema roditeljskog bloka
};
// Leksicko okruzenje sayHi funkcije
sayHiLEnvironment = {
environmentRecord: {
name: 'Summer'
},
outer: globalLEnvironment
};
Koje vrijednosti su varijable phrase
i name
imale u fazi kreiranja a koju sayHi
?
Hint: hoisting - varijable u fazi kreiranja imaju vrijednost undefined. Funkcija ima istu vrijednost.
Kako identifikator phrase
ne postoji u sayHi
kontekstu moramo pogledati u roditeljski blok koji je naznacen u leksickom okruzenju. Ovaj proces se ponavlja dok se identifikataor ne razrjesi. Ako identifikator nije pronadjen ni u globalnom kontekstu dobicemo ReferenceError
. Ovaj postupak se naziva razrjesenje identifikatora.
lexicalEnvironment
objekat se cuva u memoriji dok traje izvrsavanje funkcije ili dok postoji referenca na njega. Ako je funkcija zavrsila, ali postoji referenca na njeno leksicko okruzenje onda je jos u memoriji.
function hello() {
let greeting = 'Hello';
return function() {
console.log(greeting);
}
}
let sayHi = hello(); // kreaira novi sayHiEnvironment
// referencu na leksicko okruzenje hello()
// vrijednosti se cuvaju u memoriji dok sayHi postoji
sayHi = null; // leksicko okruzenje vise nije u memoriji
🛑 JavaScript engine nekih pretrazivaca moze dalje optimizovati i brisati svojstva ovog objekta iz memorije ukoliko optimizacija ne remeti rad aplikacije.
Zatvorenja
Zatvorenja su funkcije koje imaju referencu ka varijablama u vanjskom opsegu iz svog unutrasnjeg opsega i ako niste dosli sa tog clanka — vise o njima se moze pronaci u Sta je closure u racunarskom programiranju?
Vizuelni prikaz izvrsnog konteksta i ostalih tema
Ovdje mozete pronaci JavaScript Visualiser: Alat za vizualiziranje izvrsnog konteksta, hoisting, zatvorenja, i opsege u JavaScriptu. Podrzava ES5 sto znaci da ce let
i const
pokazati gresku jer nisu dostupni.var
opseg je na nivou funkcije, ne bloka kao u slucaju let
i const
. Iz ovog razloga leksicko okruzenje u aplikaciji mozemo vidjeti na nivou skripte ili funkcije, ne i bloka.