Praktický JavaScript (3.) - Lexikálny scope a hoisting

Publikoval Michal Kočí dňa 18.11.2016 o 19:12 v kategórii JavaScript

Dnes sa pozrieme na lexikálny scope, povieme si čo to je a prečo by sme o ňom mali vedieť, sko s ním súvisí hoisting a čo to ten hoisting vlastne je. Preto ak vám tieto pojmy nič moc nevravia, čítajte ďalej...

Lexikálny scope

Poďme najskôr na definície:

  • Lexikálny scope je miesto, kde bola premenná deklarovaná.
  • Každá premenná má svoj scope

Zatiaľ docela nuda, ale nebojte sa, čoskoro sa do toho kompletne zamotáme. Predstavte si nasledujúci príklad:

function logConstant() {
  var number = 3;
console.log(number); } logConstant();

Kde je lexikálny scope premennej number? Vo funkcii logConstant? Správne!

Tak poďme na ďalší príklad:

function logTwoConstants() {
  for(var i=0; i<2; i++) {
    var number = i * 2;
    console.log(number);
  }
  console.log(number);
}
logTwoConstants(); 

Kde je lexikálny scope premennej number teraz? Je to funkcia logTwoConstants? Alebo je to for blok? Správne, je to funkcia logTwoConstants.

Pýtal sa niekto prečo? Pretože v JavaScripte (ES5) je scope v rámci funkcie, nie v rámci bloku. Čo môže mýliť programátorov prichádzajúcich z iných jazykov céčkového typu, ale vy sa pomýliť nedajte.

Potvrdenie tohto faktu som vám nechal priamo v príklade. Ak by scope premennej number bol v rámci bloku, za for blokom by ste prístup k tejto premennej už nemali. Ale ak si príklad spustíte, uvidíte, že sa do konzoly vypíše číslo 2, t.j. posledná priradená a zároveň aktuálna hodnota.

Hoisting

Tým sa dostávame k ďalšiemu terminus technikus (teda technickému pojmu) a tým je hoisting. Krásny vznešený názov, ale nečakajte žiadnu raketové vedu.

Hoisting sa deje počas spustenia vášho skriptu a deje sa to, že deklarácie premenných nastanú ako úplne prvé v rámci scopu. Najčastejšie sa zjednodušene vraví, že deklarácie premmených sa presunú na začiatok ich scopu. Nie je to technicky úplne správne, ale na pochopenie to stačí.

Takže, ak sa vrátime k predchádzajúcemu príkladu. Ak by ste sa na funkciu logTwoConstants pozreli očami behového prostredia (t.j. toho čo ju spúšťa), videli by ste ju asi takto:

function logTwoConstants() {
  var i, number;
  for(i=0; i<2; i++) {
    number = i * 2;
    console.log(number);
  }
  console.log(number);
}

Ak ste náhodou boli v predchádzajúcej verzii tejto funkcie stále zmätený, ako je možné, že console.log za for cyklom vidí premennú number, teraz už by vám to malo byť jasné. Jej deklarácia nastane na začiatku scopu a tak je reálne jedno, kde ju vy fyzicky deklarujete.

Samozrejme, nasledovný zápis by ste isto nenapísali, ale funkčnosť funkcie je úplne identická ako jej predchádzajúce dve verzie:

function logTwoConstants() {
  for(i=0; i<2; i++) {
    number = i * 2;
    console.log(number);
  }
  console.log(number);
  var i, number;
}

Je ozaj jedno, kde presne vo funkcii var použijete, pri jej spustení sa to tvári, že ste var použili úplne na jej začiatku. Preto sa často odporúča, aby ste to písali tak, ako to "vidí" prostredie - na začiatku fukcie nadeklarovali všetky lokálne premenné. Môžete tak navyše pomôcť programátorovi, ktorý s kódom bude pracovať po vás - bude hneď vidieť, ktoré premenné sú lokálne.

Scope vo funkcii, globálny scope

Zatiaľ som tvrdil, že lexikálny scope premennej je vo funckii, v ktorej je definovaná. Toto je ale len čiastočká pravda.

Pravidlá sú trochu, ale ozaj len trochu komplikovanejšie. Sú nasledovné:

  • Ak premennú deklarujete bez použitia klúčového slova var (čo by ste nemali robiť a v striktnom móde ani nemôžete), bude jej scopom globálny scope
  • Ak premennú deklarujete s použitím klúčového slova var
    • a to v rámci funkcie, jej scope bude v tejto funkcii
    • mimo akejkoľvek funkcie, jej scope bude globálny scope

Chcete príklad? Ale samozrejme...

var globalNumber = 1;

function logSomeConstants() {
  var localNumber = 2;
  alsoGlobalNumber = 3;

  console.log(globalNumber);
  console.log(localNumber);
  console.log(alsoGlobalNumber);
}

logSomeConstants();
console.log(globalNumber);
console.log(alsoGlobalNumber);
console.log(window.globalNumber);
console.log(window.alsoGlobalNumber);

Na príklade vidíte potvrdenie môjho predchádzajúceho tvrdenia a zároveň ukážku toho, čo je to ten globálny scope. V prípade, že váš skript beží v prehliadači, globálny scope je objekt window. Takže, čo si zadefinujete na globálnom scope, k tomu viete pristúpiť priamo, alebo cez window.

V prípade, že skript spúšťate v node.js, tam samozrejme objekt window k dispozícii nie je (lebo reálne žiadne okno neexistuje), tam je globálny objekt dostupný pod názvom global.

Praktické dôsledky a využitie

Teória je to zaujímavá, ale k čomu vám všetky tieto vedomosti sú v praxi? Hneď na prvý pohľad viete povedať, či bude premenná lokálna, alebo globálna. A to je dôležité.

Totiž, predstavte si, že váš skript neexistuje v stránke osamotene, čo je veľmi pravdepodobné. Stránka načítava viac skriptov a pokiaľ viaceré z nich deklarujú globálne premenné a navyše ich názvy sú rovnaké, tak ste v... vo veľmi ošemetnej situácii. Ide o tzv. kolíziu názvov a takéto problémy naozaj riešiť nechcete, verte mi.

Druhým problémom, ktorý je v mierne teoretickej rovine je rýchlosť vašej aplikácie. Pokiaľ niekde pristupujete k premennej, behové prostredie ju musí nájsť.

Napríklad výraz console.log(number) znamená, že chcete spustiť funkciu log na objekte console a chcete jej ako hodnotu prvého parametra poslať hodnotu premennej number.

Prostredie sa teda pozrie, či na aktuálnom, lokálnom scope máte deklarovanú premennú tohto názvu. Pokiaľ máte, použije ju (resp. jej hodnotu).

Pokiaľ nemáte, behové prostredie sa pozrie ďalej - na globálny scope. Teda, prvé teoretické spomalenie nastane v dôsledku toho, že musí prehľadať nie jeden, ale dva scopy.

No a druhé spomalenie môže nastať preto, lebo je globálny scope zapratený obrovským množstvo premenných, ktoré tam nasekali programátori neuvedomujúc si dôsledky a navyše pravdepodobne úplne nezmyselne. Globálne premenné sú potrebné veľmi, naozaj veľmi zriedka.

V ďalšom diele uvidíte...

Keď už chápete lexikálny scope, nabudúce sa pozrieme na fantastickú vec a tou budú closures. A vysvetlíme si ako fungujú, aby vás nič neprekvapilo. Takže, určite ostaňte naladení...

Chcete sa poriadne naučiť JavaScript a ešte sa pritom naučiť nejaký framework či knižnicu (napríklad Angular či React)? Prídite na niektoré z mojich JavaScriptových školení a naučte sa to rýchlo a jednoducho.

Mohlo by ťa tiež zaujímať

Páčil sa ti príspevok?

Zdieľaj príspevok alebo si ho odlož na neskôr

Sleduj ma

Ak nechceš premeškať príspevky ako je tento, sleduj ma na Twitteri, alebo ak máš RSS čítačku, môžeš sledovať môj RSS kanál.

Komentáre

Waldo dňa 12.12.2016 o 15:19 - Globálne premenné

Je to trochu nepresne vysvetlené. Ak vo funkcii vynechám kľúčové slovo var, tak som nevytvoril novú globálnu premennú, ale novú vlastnosť objektu window. Čo nie je to isté. Napríklad danú vlastnosť môžem zmazať cez operátor delete, ale skutočnú globálnu premennú (definovanú cez var mimo funkcie) zmazať nemôžem. Ale inak článok chválim. Zrovna takéto veci treba vysvetľovať, lebo hoisting, scope, lexical closures a podobne sú najviac nepochopené vlastnosti.

Waldo dňa 12.12.2016 o 15:33 - PS:

A ešte ... hoisting by bolo dobré vysvetliť na funkciách, lebo najviac tam pletie, že zdvíhané sú len definície premenných, nie definície celých funkcií - vrátane tela funkcie.

Pridať komentár

Máš niečo zaujímavé povedať k článku? Pridaj to k článku ako komentár. Spam, reklamu alebo inak nerelevantné komentáre okamžite mažem.

Pridanie komentára sa nepodarilo. Oprav si prosím chyby.