webdevlpr

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:

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

  1. Pravi se variableObject koji sluzi za pocetno skladistenje varijabli, argumenata i deklarisanih funkcija.
  2. Pravi se lexicalEnvironment kao kopija variableObject u ovom trenutku.
  3. Definise se vrijednost this i pokazuje na objekat na koji se funkcija primjenjuje u tom kontekstu.

U fazi izvrsenja izvrsnog konteksta

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

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.