A JavaScript mélységei: Környezetismeret

💡
EDIT: A cikk az új JavaScript szabvány megjelenése előtt íródott. Így a környezetekre vonatkozó állítások, az új szabvány esetén csak a var kulcsszóval definiált változókra igazak.
while(false) { var alma; }

Ez a sor nem csinál semmit. Vagy mégis? Ez a mai kérdésünk.

Aki tudja a választ (és az okát is), az ugorjon az izgalmas részhez!

Mint azt a múlt héten láttuk, az alma változó értéke undefined típusa pedig Undefined, de csak akkor ha a deklarációját leírjuk, illetve ha a deklarációja meg is történik, ergo lefut a "var alma;" utasítás. Viszont mivel ez a deklaráció egy olyan előltesztelős ciklusban került elhelyezésre, amely logikai értéke biztosan hamisra értékelődik ki, a hozzárendelt utasítás nem fog lefutni. Így aztán a változónk deklarálatlan lesz, tehát ha megpróbáljuk kiolvasni, akkor egy ReferenceError-t kellene kapnunk (hasonlóan ahhoz, ahogy például C-ben egy ilyen próbálkozás le sem fordul).

Hát akkor próbáljuk meg!

Először nézzük meg, hogy az előző rész végén igazat mondtam-e:

Screenshot 2014-05-11 15.34.55

Nem deklaráltuk, azaz nem is létezik, szóval igazat mondtam. Remek, jöhet a mai kérdés:

Screenshot 2014-05-11 15.37.42

Hoppá. Ez nem igazán dobott hibát, sőt kiírta, hogy undefined, vagyis lefutott a "var alma;" utasítás. Ezennel ismét megérkeztünk a témánkhoz, hiszen egyetlen oka lehet ennek a váratlan viselkedésnek, mégpedig az, hogy a JavaScriptet nem igazán izgatja, hogy más blokkban, azaz más környezetben lett deklarálva a változónk, mint ahol olvasni próbáljuk. Szóval akkor nincsenek környezetek? Természetesen ahogyan típusok, úgy környezetek is vannak a JavaScriptben, csak ezek sem egészen úgy működnek, mint ahogyan azt várnánk, de szokás szerint kezdjük kicsit távolabbról.

Identifier Resolution

A identifier resolution az a módszer, amely azért felel, hogy a fordító (jelen esetben interpreter) el tudja dönteni, hogy egy név (identifier) mit jelent a kód egy adott részén. A JavaScriptben, ahogyan a legtöbb másik nyelvben is, ez a környezetek (environments) segítségével történik, méghozzá egy egyre tágabb körben történő kereséssel. Azaz először a lokális változók között kezd el keresni az algoritmus, aztán ha ott nem talál semmit, akkor megy feljebb mondjuk az adott objektum szintjére, aztán még feljebb és feljebb, míg végül elér a globális kontextusba. Ha ott sem találja meg a nevet, akkor az nem létezik.

A JavaScript esetében mivel a fordítás és a futtatás nem különül el egymástól, a környezeteket már egy korai stádiumban (a statement parserben) létrehozza az interpreter, így futás időben már rendelkezésre áll a Lexical Environment, így az identifier resolution már ténylegesen a környezetekben keres.

Lexical Environment

Azaz a lexikális, tehát a forráskód konkrét szövegén alapúló környezet. Ezt tulajdonképp (bár nem tökéletes a hasonlat) tekinthetjük a JavaScriptes header fájlnak, amely tartalmazza az adott környzetben deklarált összes változót, illetve egy mutatót az őt tartalmazó környezetre (ha létezik ilyen). Futáskor a kód természetesen ismeri a saját környezetét, így az interpreter ezt a környezetet és a mutatókat használva, bármilyen messze is legyen, mindig képes megtalálni a referenciát (és ezáltal az értéket) egy-egy névhez.

Környezetek a JavaScriptben

A kérdés csak az, hogy mikor keletkezik új környezet. Nyílván létrejön egy, amikor elkezdjük feldolgozni a bemenetet. Ez lesz a globális kontextus. Továbbá létrejön minden függvényhez egy új környezet az őt körülvevő környzet részeként, tehát általában a globális kontextusban. Ez azért szükséges, hogy a függvények létrehozhassanak lokális változókat, melyeket  globális kontextusból nem, viszont a függvényből és a benne létrejött környezetekből el lehet érni. Azonban az elágazások (if, switch) és az iterációk (for, for..in, while, do...while) nem hoznak létre új környezetet, azaz JavaScriptben nem létezik blokk kontextus. (EDIT: De létezik, az új let kulcsszó segítségével) Tehát egy ciklus belsejében deklarált változó nem a ciklus, hanem a ciklust tartalmazó környezet része lesz. Ezért akkor is deklarálódik, ha a ciklus logikai értéke hamisra értékelődne ki, azaz ha a ciklus egyszer sem fut le!

A függvényen kívül még két konstrukció hoz létre új környzetet. Az első a with kulcsszó. Ez lényegében arra szolgál, hogy mi magunk tudatosan új környzetet teremtsünk egy meglévő környzeten belül. Erre egy későbbi cikkben még visszatérünk.

A másik eset a catch kulcsszó, amely azért hoz létre új környezetet, hogy a lekezelendő hiba kizárólag a catchhez tartozó blokkból legyen elérhető. Tehát fontos megjegyezni, hogy nem a catch blokkja az új környezet, csupán a hibát tároló változó (amit általában e, vagy exc névvel illetünk) tartozik az új környezethez, tehát ez sem blokk kontextus.

This is it!

Két fontos dolog maradt a végére. Egyrészt a this-ről még nem beszéltünk, ebben a részben már nem is fogunk. De az nagyon fontos, hogy a Lexical Environment és a this két különböző dolog.

Másrészt a Lexical Environment csak akkor tárol értékeket, ha az adott kód ténylegesen le is futott és az értékadások megtörténtek. Azaz:

Screenshot 2014-05-11 16.53.01

Tehát az értékadástól (inicalizációtól) függetlenül undefined lett a kiírt érték, hiszen a while blokkja nem futott le, mivel a false mindig hamisra értékelődik ki. Csupán az interpreter által felépített lexikális környezetben szerepel az alma, természetesen a megszokott alapértékkel (undefined).

Ezzel a mai kérdést meg is válaszoltuk, de az izgalmas rész még csak most következik...

Az izgalmas rész

"use strict";
 
function valami() {
  if (true) {
    alma = 52;
  }
  else {
    var alma = 42;
  }
 
  alert(alma); //A
}
 
valami();
 
try {
  alert(alma); //B
}
catch(e) {
  alert(e); //C
}

Ez aféle "mit ír ki" feladat, mint amiket az egyetemen szoktak adni: Melyik alert-ek jelennek meg és mit írnak ki?

Kezdjük a "use strict";-el. Az ECMAScript 5-ös verziójában jelent meg a strict mód, amely sok egyéb mellett megtiltja a var kulcsszó használata nélkül való változó deklarációt. Ugyanis normálisan, ha nem írjuk ki a var-t, akkor a lokális környezet  helyett a globálisban jön létre a változó, ergo akár egy függvény belsejében is deklarálhatunk olyan változót, amit aztán a kód bármely részéről elérünk (feltéve, hogy a függvény legalább egyszer futott). Ez nagy ritkán hasznos lehet, de az esetek többségében inkább hibához vezet, hiszen egy elírt változónév ahelyett, hogy hibát okozna inkább definiálódik. Pont ezért tíltja a strict mód.

Viszont első ránézésre ez a kód pont valami ilyesmit csinál, hiszen az "alma = 52;" eleve azelőtt szerepel, hogy deklarálva lenne és ráadásul a deklarációs kód még csak le sem fut soha, hiszen pont az else ágba került. Szóval elvben hibát kellene dobnia.

De nem dob, mivel ahogy azt feljebb már írtam a deklaráció a futástól függetlenül történik. Sőt mitöbb a futás előtt, így az sem gond, hogy több sorral a deklaráció előtt adunk értéket a változónak.

Szóval az első alert (A) megjelenik és az 52 szerepel benne mint érték.

Nézzük a másodikat. Egy olyan változót próbálunk kiolvasni, ami a valami függvényen belül került deklarálásra, tehát a függvény környezetéhez tartozik. Így nyílván nem lehet elérni, tehát a második alert (B) nem jelenik meg, helyette a harmadik (C) lép működésbe és a tartalma egy ReferenceError lesz.

Az érdekes a dologban az, hogy annyira igaz, hogy korábban történik a deklaráció, mint a futás, hogy ha a strict módot kikapcsolnánk, akkor is pontosan ugyanez történne, ugyanis az "alma = 52;" mindenképp az else ágban lokálisan definált változónak ad értéket.

A válasz kipróbálható változata itt található: http://jsfiddle.net/4zZx7/. Érdemes kísérletezni vele egy kicsit...

Ezzel az e heti cikk végéhez értünk, jövő héten egy kevésbé elvont témával folytatjuk.

Utóirat: A szomorú igazság

Sajnos a valóságban ez nem ennyire egyszerű, úgyhogy lesz még erről szó. Addig is álljon itt dőlt betűvel a tömör igazság:

A specifikáció két környezetet határoz meg, az egyik a VariableEnvironment, a másik pedig a LexicalEnvironment. Típus tekintetében midkettő Lexical Environment (külön írva!), azonban míg a VariableEnvironment tényleg a statement parser által kerül létrehozásra és a FunctionDeclaration-ök, illetve a VariableStatement, továbba a WithStatement típusú statementek kiértékelésekor kerül feltöltésre, addig a LexicalEnvironment már futás időben jön létre a VariableEnvironment másolataként és a különböző dinamikus Binding-ok segítségével kerülnek benne meghatározásra a tényleges Identifier-Reference összerendelések. A this kulcsszó által mutatott ThisBinding pedig egy a mindkettőtől elkülönülő harmadik pillére az Execution Contextnek, amely a futó kód tényleges környezete.