Lexikální prostředí, rozsah a uzávěr v JavaScriptu. Vysvětleno

Přestaňte hádat a začněte vědět. Praktický rozbor toho, jak JS engine spravuje proměnné, proč funkce je objekt a jak díky pochopení vnitřních mechanismů psát čistší kód bez zbytečných chyb.
Nálepky:
Toto téma zní trochu děsivě (a bohužel ne jen tak pro nic za nic). Mnozí vývojáři, občas i velmi zkušení, se těmto tématům a všem komplikovaným situacím buď úplně vyhýbají, nebo čtou tutoriály, které je vysvětlují přes odtržené a zavádějící analogie. Kvůli tomu mají stále bílá místa a pocit, že jde o nějaké kouzlo, kterému není možné porozumět.
A když narazí na nezvyklou konstrukci v kódu, kde není na první pohled zřejmé, jak by se měl JS engine zachovat a jakou hodnotu by měl odněkud vzít, prostě se tomu snaží vyhnout, nebo se trápí nepředvídatelností během debuggingu. Tohle přináší stres, kterého je v naší profesi občas i bez toho zbytečně moc.
Takže aby bylo stresu méně a zkušených, sebevědomých a šťastných českých programátorů, kteří jsou ve svém oboru jako ryby ve vodě, bylo více, dnes vám povím, jak to funguje ve skutečnosti – na úrovni mechanismů a specifikace. Co je to Lexical Environment, co je Scope a Closure, v čem je mezi nimi rozdíl a v čem naopak vazba. A přitom doufám, že to zvládnu lidskou řečí a srozumitelně.
Takže pojďme na to:
Nejprve stručně
- Lexical Environment – Lexikální prostředí – tohle je naprosto skutečna věc v paměti zařízeni. I když nemáme z našeho kódu k tomu Lexical Environment žádný přistup, nicméně to je naprosto skutečna věc v paměti, díky které engine javascript funguje tak, jak funguje.
- Closure – Uzávěry – to je teoretický termín, který znamená samotnou funkci a připojené k ně Lexikální prostředí.
- Scope – Dosah platnosti – Rozsah platnosti – toto je taky teoretický termín který znamená “množství veškerých viditelných proměnných z tohoto nebo jiného bodu kódu”
No a abychom se na to podrobněji podívali, začneme trochu z dálky:
Funkce v javascript – co to je a jak funguje?
Funkce v javascript – to je objekt. Callable objekt, ale nicméně objekt.
Ano, přesně tak, funkce v javascriptu – to je objekt!
Pokud nevěříš, tak spušt’ tento kód v konzoli a přesvědč se:
function doSmth() {}
console.log(doSmth instanceof Object); // true
Code language: JavaScript (javascript)
Pro ještě větší přesvědčivost:
function sayHi() {
if (sayHi.userName) {
console.log(`Ahoj, ${sayHi.userName}!`)
} else {
sayHi.userName = prompt("Ahoj! Zadej své jméno:");
sayHi()
}
}
sayHi.userName = "Andrij";
sayHi() // "Ahoj, Andrij!"
const borkedObject = sayHi;
borkedObject.userName = "Ondřej";
sayHi() // "Ahoj, Ondřej!"
borkedObject() // "Ahoj, Ondřej!"
console.log(typeof borkedObject) // "function"
delete borkedObject.userName;
sayHi() // hned prompt, a pak "Ahoj, --tady tebou zadaná hodnota--!"
console.log(borkedObject.userName) // tebou zadaná hodnota
const pristineObject = {
...sayHi
};
console.log(pristineObject) // { userName: tebou zadaná hodnota }
Code language: JavaScript (javascript)
Když jsem se o tom poprvé dozvěděl, měl jsem myšlenku: to je blbost! To jde proti logice. Proč dělat funkce jako object a co vůbec mají společného?! Objekt je vymyšlený k tomu, aby chránil nějaká data, a funkce vymyšlena k tomu, aby něco dělala! Udělali kočkopsa. Jaká hrůza, jaký nesmysl, aaa!
Ale pak jsem zhluboka vydechl, chvilku přemýšlel a pochopil:
Knížka s recepty nepeče pizzu. Knížka s recepty prostě chrání sadu instrukcí.
Funkce v javascriptu – to je též něco jako kniha s recepty. Když se vyvolává funkce sayHi(), tak engine začíná postupovat podle těch instrukcí, které jsou v té funkci zapsané. JavaScript engine ngine začne hledat a měnit hodnoty proměnných, vyvolávat browserové API (console.log, prompt) atd. Ale funkce sama o sobě jen chrání sadu těchto instrukcí, sama nic nedělá.
Představ si, kdybychom místo:
const logSomething = function (something) {
console.log(something);
};
Code language: JavaScript (javascript)
psali:
const logSomething = new Function(
// pišeme, jaké parametry chceme obdržet
"data",
// píšeme co dělat
"console.log(data)",
);
Code language: JavaScript (javascript)
A představ si, že bychom v důsledku toho neměli nějakou magickou abrakadabru, ale objekt:
{
"parameters": ["data"],
"whatToDo": "console.log(data)"
}
Code language: JSON / JSON with Comments (json)
Představil(a) sis to?
No tak přesně tak to funguje. Spust’ tento kód v konzoli a přesvědč se:
function doSmth() {}
console.log(doSmth instanceof Function);
console.log(Object.prototype.isPrototypeOf(Function));
Code language: JavaScript (javascript)
Ted’ const logSomething = new Function("data", "console.log(data)") vyvolá chybu. Ale chyba bude ne proto, že by konstruktor Function neexistoval, nýbrž kvůli pravidlům bezpečnosti (This document requires 'TrustedScript' assignment. The action has been blocked.). Tedy engine správně chápe new Function(), jen prostě přerušuje vykonávání kódu z ohledu na bezpečnost.
No ale bavili jsme se přece o lexical environmentu, ne?
Máš pravdu. Dědka to nějak vzalo. Vrátíme se do lexical environmentu.
V předchozích přikladech je všechno příliš prosté. Uvnitr funkce my používáme pouze proměnnou data, která přicházi jako argument.
Ale co když budeme chtít odkázat na nějakou jinou, vnější proměnnou?
Odkud přesně má engine brát hodnotu té vnější proměnné? No?
Z místa, kde byla funkce vyvolána? Ano. Ale jak má vědět, kde byla tato funkce vyvolána?
Engine sám o sobě neví nic. Aby on něco “věděl”, musí to něco nějak uložit v paměti zařízení.
Takže aby ten engine fungoval tak, jak funguje, programátor, který psal ten engine, přišel na lexical environment.
Když engine začne postupovat podle kódu, engine pro každý jednotlivý blok kódu vytváří v paměti zařízení oddělený lexical environment.
V těchto lexical environmentech engine chrání [[Environment Record]] – podobjekt, který chrání všechny proměnné, které byly deklarovány uvnitř tohoto bloku kódu, a taky [[Reference to the Outer Lexical Environment]] – odkaz na lexical environment vnějšího bloku.
Aby se to dalo lépe představit, využijeme příklad:
// lex-env-0 pro celý script
// (Ve skriptu nejsou žádné složené závorky, ale je to také jako jeden velký blok kódu)
let animal = "jeżyk";
if (true) { // lex-env-1
animal = "bóbr";
}
if (true) { // lex-env-2
const nickname = "ku_wa";
if (true) { // lex-env-3
const salutation = "ja pierdolę, jakie bydlę!";
console.log(animal, nickname, salutation);
console.log(unexistingSmth);
}
}
Code language: JavaScript (javascript)
Když engine začíná postupovat podle toho kódu, vytváří pod kapotou něco, co se dá zjednodušeně zobrazit takto:
{
"lex-env-0": {
"Environment_Record": {
"animal": "bóbr"
},
"Outer_LE_id": null
},
"lex-env-1": {
"Environment_Record": {},
"Outer_LE_id": "lex-env-0"
},
"lex-env-2": {
"Environment_Record": {
"nickname": "ku_wa"
},
"Outer_LE_id": "lex-env-0"
},
"lex-env-3": {
"Environment_Record": {
"salutation": "ja pierdolę, jakie bydlę!"
},
"Outer_LE_id": "lex-env-2"
}
}
Code language: JSON / JSON with Comments (json)
Když je na řádku 7 (animal = "bóbr") v kódu napsáno, že je nutné změnit hodnotu proměnné animal, engine se okamžitě pokusí najít proměnnou s názvem animal v lexikálním prostředí stejného bloku (lex-env-1). Ale tam tato proměnná není. Takže engine přechází dále podle Reference to the Outer Lexical Environment (odkazu na vnější lexikální prostředí) v lex-env-0 a tam tato proměnná skutečně je. A tam engine tuto proměnnou mění.
Když je na řádku 16 nutné provést console.log(animal, nickname, salutation), engine se okamžitě pokusí najít proměnnou s názvem animal v lexikálním prostředí stejného bloku lex-env-3, ale taková proměnná tam není. Poté engine přechází podle Reference to the Outer Lexical Environment do lex-env-2, ale ani tam proměnná animal není. Proto engine přechází podle odkazu do lex-env-0, kde ji nakonec nachází. Pak engine podle stejného principu hledá proměnnou s názvem nickname a proměnnou s názvem salutation.
A když na řádku 20 je nutné provést console.log(unexistingSmth), tak engine stejně hledá unexistingSmth hned v lex-env-3, pak v lex-env-2, pak v lex-env-0 a jelikož lex-env-0 nemá Reference na vnější lexikální prostředí, tak engine přeruší vykonávání našeho kódu a zachytí chybu: “unexistingSmth is not defined”.
Vracíme se k funkcím
Výše jsme si už ujasnili, že funkce je objekt, který v sobě uchovává kód k provedení a parametry, které je třeba si vyžádat.
Také jsme zjistili, že pro každý blok kódu (pro každý, nejen pro funkce) během ho spuštění vytváří engine v paměti zařízení tzv. lexical environment, kde uchovává všechny proměnné deklarované v daném bloku a odkaz na lexical environment nadřazeného bloku kódu.
Nastal čas složit puzzle 🪄
V příkladu, který jsme rozebírali výše, se všechny bloky kódu vykonávají pouze jednou a na jednom místě. Funkci ale engine může spouštět mnohokrát a na mnoha různých místech.
Pokaždé, když se engine pokusí vykonat kód uložený v dané funkci, pokaždé musí někde najít hodnoty proměnných, se kterými kód funkce pracuje.
A právě proto se tvůrci JS rozhodli, že funkce bude ukládat nejen kód a parametry, ale také právě onen „Reference to the Outer Lexical Environment“.
Proto když píšeme:
const logSomething = function (data) {
console.log(data);
};
Code language: JavaScript (javascript)
tak engine v ten moment vytváří new Function(), která v sobě nese nejen whatToDo: "console.log(data)" a parameters: ["data"], ale také odkaz na lexikální prostředí toho bloku kódu, kde byla funkce deklarována. Přičemž tento odkaz na lexikální prostředí bloku, ve kterém byla funkce deklarována, zůstává s funkcí navždy. I když ji někam předáme jako parametr nebo ji zkopírujeme. Funkce si vždy nese reference na lexikální prostředí bloku, kde vznikla.
Právě tato vlastnost umožňuje triky jako tento:
let incrementAndReturnClosedCounter;
if (true) {
let closedCounter = 0;
incrementAndReturnClosedCounter = function () {
return ++closedCounter;
};
}
console.log(incrementAndReturnClosedCounter()); // 1
console.log(incrementAndReturnClosedCounter()); // 2
console.log(incrementAndReturnClosedCounter()); // 3
closedCounter++; // Chyba. closedCounter is not defined
Code language: JavaScript (javascript)
Pozor na velmi důležitý detail!
Uvnitř funkce není uložen samotný lexical environment, ale pouze odkaz na něj (na ten nadřazený). Do funkce se „všívá“ jen odkaz na vnější lexikální prostředí.
Ale!
Vlastní lexikální prostředí (prostředí samotné funkce pro kód, který je v ní uložen) se vytváří znovu při každém spuštění daného kódu.
Ještě jednou: odkaz na vnější lexical environment se ve funkci ukládá navždy. Ale samotný lexical environment spuštěného kódu se při každém startu vytváří nanovo.
Proto se všechny proměnné deklarované uvnitř funkce při každém spuštění vytvářejí znovu.
const incrementAndReturnInnerCounter = function () {
let innerCounter = 0; // pokaždé se vytvoří znovu
innerCounter++;
return innerCounter;
};
console.log(incrementAndReturnInnerCounter()); // 1
console.log(incrementAndReturnInnerCounter()); // 1
console.log(incrementAndReturnInnerCounter()); // 1
Code language: JavaScript (javascript)
Odkaz (reference) na vnější lexikální prostředí se předává vnitřnímu prostředí. Ale samotné vnitřní prostředí je pokaždé nové.
Cvičení na procvičení
Koukni na tenhle kód a zamysli se, proč nevyhodí chybu. Proč sayHi, které volá sayHi, funguje?
const sayHi = function () {
if (sayHi.userName) {
console.log(`Ahoj, ${sayHi.userName}!`);
} else {
sayHi.userName = prompt("Ahoj! Jak se jmenuješ?");
sayHi();
}
};
sayHi();
Code language: JavaScript (javascript)
Když píšeme function sayHi() {..... – je to v podstatě totéž, jako říct const sayHi = new Function(..... Tedy engine v environment recordu uvnitř lexikálního prostředí celého skriptu vytvoří proměnnou jménem sayHi a do ní vloží objekt-funkci, která má v sobě kód a outer reference na prostředí, kde vznikla:
{
"lex-env-0": { // lexikální prostředí celého skriptu
"Environment_Record": {
"sayHi": {
"type": "function",
"whatToDo": "if(sayHi.userName){console.log(`Aho..",
"parameters": [],
"reference_to_outer_LE": "lex-env-0"
}
},
"Outer_LE_id": null
}
}
Code language: JSON / JSON with Comments (json)
Poté, když zavoláme sayHi() – dáváme příkaz k vykonání kódu. Engine vytvoří lexikální prostředí pro toto konkrétní spuštění (lex-env-1) a paměť vypadá zhruba takto:
{
"lex-env-0": { ... },
"lex-env-1": {
"Environment_Record": {}, // prázdno, uvnitř nejsou lokální proměnné
"Outer_LE_id": "lex-env-0" // převzato z reference_to_outer_LE funkce
}
}
Code language: JavaScript (javascript)
Engine pak krok za krokem vykonává instrukce. Když dojde na if(sayHi.userName) – engine nejdřív hledá sayHi v lex-env-1. Tam není. Tak jde po referenci do lex-env-0 a tam ji v klidu najde. Proto žádná chyba, vše šlape.
Teď zkus sám odhadnout, co se stane tady:
function createSayAbrakadabra() {
return function () {
console.log(sayAbracadabra);
};
}
const greet = createSayAbrakadabra();
greet();
Code language: JavaScript (javascript)
A co se stane tady:
function createSayAbrakadabra() {
return function () {
console.log(sayAbracadabra);
};
}
const greet = createSayAbrakadabra();
let sayAbracadabra = "Abrakadabra!";
greet();
sayAbracadabra = createSayAbrakadabra();
greet();
sayAbracadabra();
createSayAbrakadabra()();
Code language: JavaScript (javascript)
Abych nedělal spoilery, nic jsem nepsal. Správnost svých úvah si ověř spuštěním kódu. Pokud budou dotazy, čekám v komentářích.
Pokud jsou tvé odpovědi špatně – doporučuji si to přečíst znovu a pokud to nepomůže, tak napiš komentář a já to tam vysvětlím.
O Scope a Closure
Výše jsem hodně mluvil o lexical environment (protože je to nejtěžší). Předpokládám, že jsem ti z toho udělal v hlavě slušný hokej.
Promiň, ale takhle svět funguje. Kdyby se dal JS pochopit jedním TikTok videem, nemělo by to porozumění žádnou hodnotu a nikdo by za to neplatil. Bohužel (nebo bohudík) to tak není. Takže vydrž a potrap hlavu :)
Takže:
Jak jsem říkal – lexical environment je naprosto reálná věc. Lexikální prostředí skutečně existují v paměti zařízení.
Naproti tomu scope a closure jsou teoretické termíny. Slova, kterými popisujeme jevy.
Closure (uzávěra) – je slovo, kterým se myslí funkce (její vlastní environment vytvořený při spuštění) PLUS všechna lexikální prostředí, která jsou této funkci dostupná (přes řetězec outer referencí).
Scope (oblast viditelnosti) – je slovo, kterým se myslí proměnné, které jsou dostupné z toho či onoho místa v kódu.
Příklad:
const userName = "Jakub";
function getRole() {
const randomNumber = Math.random();
if (randomNumber > 0.9) {
return "admin";
} else if (randomNumber > 0.7) {
return "moderator";
} else {
return "běžný uživatel";
}
}
Code language: JavaScript (javascript)
Kolik a jakých prostředí engine vytvoří, si můžeš spočítat sám.
Uzávěra (closure) je zde samotná funkce getRole spolu s odkazem na vnější lexical environment, kde jsou uloženy userName a getRole.
A oblast viditelnosti (scope) závisí na tom, z jakého bodu v kódu se díváme. Pokud z řádku 4, tak do scope patří randomNumber, getRole a userName. Pokud se díváme z řádku 14, tak do scope patří jen userName a getRole.
Malá oprava
Někdy se scope netraktuje jako množina proměnných, ale jako pravidlo, podle kterého se určuje, co má být vidět.
Bohužel to tak historicky dopadlo. Chytří lidé nevymysleli dvě různá slova a vznikl zmatek, kdy jedno slovo (scope) může znamenat dva podobné, ale odlišné pojmy.
Já konkrétně zde beru scope ve významu, v jakém se používá v devtools a většinou v praxi: jako množinu dostupných proměnných.
Ještě jednou krátce pro shrnutí
Lexical environments (lexikální prostředí) – reálné věcí v paměti. Součást mechanismu enginu, díky kterému si pamatuje, kde je jaká proměnná.
Closure (uzávěra) – teoretický termín: „funkce a proměnné, které jsou jí dostupné“.
Scope (oblast viditelnosti) – množina proměnných viditelných z daného místa. Trochu se podobá samotnému prostředí, ale pozor: lexical environment je čistě JS věc v paměti. Scope je teoretický termín používaný i mimo JS.
Dva detaily
Častečné zjednodušení
Probrali jsme jen část hodnot, které funkce uchovává. Je jich více. Například jaký je typ té funkce (normální, asynchronní, arrow), zda je funkce v strict módu nebo ne a další. Samotný Environment Record v sobě také obsahuje další věci. Neuváděl jsem zde všechno, ale pouze to, co právě potřebujeme.
Také je důležité pamatovat na to, že všechny tyto hodnoty jsou „pod kapotou“ a v paměti samozřejmě nejsou ve formátu JSON. Formát JSON byl použit pouze pro přehlednější vizualizaci. Stejně tak názvy hodnot, které v sobě funkce uchovává, se ve specifikaci mírně liší – například místo „whatToDo“ je tam [[ECMAScriptCode]] a místo parameters je to [[FormalParameters]].
Je to asi jasné, ale pro jistotu: tento článek není náhradou specifikace, ale pouze adaptovaným vysvětlením její části.
new Function
Výše jsem na to nekladl důraz. Může to působit, že const f = new Function() a const f = function() {} je to samé.
Je to složitější. I kdyby tě prohlížeč nechal spustit new Function(), je tam rozdíl. Když engine vytváří funkci přes new Function(), do jejího vnějšího odkazu (Outer Lexical Environment) uloží odkaz na globální prostředí celého dokumentu, nikoliv na prostředí nejbližšího bloku kódu.
Ale znovu: i kdybys obešel bezpečnostní omezení, vytvářet funkce přes new Function() je špatná praxe. Nedělej to.
Závěr
Doufám, že se mi podařilo nejen zamotat, ale i vymotat.
Pokud byl příspěvek užitečný nebo zůstaly otázky, klidně napiš komentář.
Ahoj!

Stop stop stop!
A co ten function declaration vs function expression?
A co ten var vs let & const;
A co ten sloppy vs strict mode?
To se taky týká toho tématu, a pokud už jsem to začal, tak musím vysvětlit i to.
Ale PŘEDEM PŘEČTI PROSÍM TYTO 2 VĚCI:
- Pokud jsi dokázal přečíst a porozumit popsanému výše, tvůj mozek může být i tak trochu zavařený. Dej si odpočinek a vrať se sem zítra.
- „var vs. let & const“ a další – to nejsou tak důležité věci. Je dobré aspoň něco o tom vědět, ale je to také strašně komplikované a propletené legacy JavaScriptu a nemusíš tomu věnovat zbytečně moc času a úsilí. Pamatuj na balanc.
No a pokud sis dal odpočinek a stále tě to zajímá, tak pojďme na to:
Výše už jsem říkal, že pro každý blok kódu engine vytváří lexical environment; že každý blok kódu má svůj lexical environment. To je pravda.
Ale lexical environment vymysleli až v roce 2009. Do té doby se používal jeho starší a horší analog – variable environment. Který kvůli zpětné kompatibilitě nechali dodnes. Proto engine současně pracuje jak s lexical environment, tak s variable environment.
Variable environment je taky taková věc v paměti, hodně podobná lexical environment. Ale variable environment engine vytváří pouze pro celý dokument, pro konkrétní skript a pro kód funkce při každém spuštění daného kódu. Ale pro jednoduché bloky kódu, jako jsou if nebo cykly, ne. Celý dokument, celý skript a kód, který byl spuštěn z funkce, mají jak lexical environment, tak variable environment, ale jednoduché bloky kódu mají pouze lexical environment.
Takže když deklarujeme funkci pomocí var, engine nepoužívá lexical environment, ale variable environment. Proměnnou, která je deklarovaná přes var, engine neukládá do nejbližšího lexical environmentu (který mají všechny bloky), ale do nejbližšího variable environmentu (který ty jednoduché bloky prostě nemají). Proto to vypadá, jako by proměnná „vyskakovala“ za hranice bloku. Přitom nikam „neskáče“, engine ji prostě ukládá do nejbližšího právě variable environmentu, který běžný blok prostě nemá.
Teď co se týče function declaration (function f() {}) vs function expression (const f = function() {}) (dej lajk, jestli ty názvy taky pořád pleteš).
Pokud function expression f = function(){} následuje po const nebo let, engine ji uloží do nejbližšího lexical environmentu, ale pokud následuje po var, tak do nejbližšího variable environmentu. Analogicky jako při deklaraci proměnné. Přitom ale engine ve všech případech funkci stejně přišije odkaz na vnější LE. I když je funkce deklarovaná po var, engine jí stejně přišije odkaz na vnější LE. Přičemž přišije odkaz právě na lexical environment toho bloku, kde byla deklarována. A vznikne z toho takovýhle úlet:
if (true) {
const hiddenVariable = "Nedívej se, stydím se.";
var arrowFunctionAfterVar = () => {
console.log(hiddenVariable);
}
var functionExpressionAfterVar = function() {
console.log(hiddenVariable);
}
}
arrowFunctionAfterVar(); // "Nedívej se, stydím se."
functionExpressionAfterVar(); // "Nedívej se, stydím se."
console.log(hiddenVariable) // ReferenceError: hiddenVariable is not defined
Code language: JavaScript (javascript)
Situace s function declaration (function f() {}) je ještě zamotanější. Pokud se kód spouští ve starém režimu (sloppy mode), tak engine uloží function declaration do variable environment (které prosté bloky nemají). Ale přitom jí odkaz na vnější LE engine stejně přišije. Přičemž přišije odkaz právě na lexical environment toho bloku, kde byla deklarována. A vznikne podobný úlet jako v předchozím případě:
if (true) {
const closedVariable = "Nedívej se, stydím se.";
function declaredFunction () {
console.log(closedVariable);
};
}
declaredFunction(); // Nedívej se, stydím se.
console.log(closedVariable); // ReferenceError: closedVariable is not defined
Code language: JavaScript (javascript)
Ačkoli samotná closedVariable není mimo blok dostupná, funkce declaredFunction je na tento blok uzavřená (tedy odkaz na vnější LE funkce declaredFunction vede na LE bloku, a ne celého skriptu) a pro kód declaredFunction je proměnná closedVariable dostupná. Přitom ačkoliv declaredFunction byla také deklarována uvnitř if, na rozdíl od proměnné po const byla funkce deklarována pomocí function declaration ve sloppy módu, a uložena ne do lexical environmentu bloku kódu, kde byla deklarována, ale do variable environment celého skriptu (protože blok if variable environment nemá).
Pokud se kód spouští v novém, přísném strict módu, pak proměnné deklarované po var bude engine i nadále ukládat do nejbližšího variable environment (ačkoliv adekvátní lidé ve strict módu var nepoužívají). Ale funkce vytvořené pomocí function declaration (function f() {}) bude engine ve strict módu ukládat do lexical environment.
Poslední příklad:
// ⚠️ tohle je sloppy mode
if (true) {
var variableAfterVar = "Toto je hodnota proměnné po var";
function functionDeclaration() {
console.log("Tohle uvidíš");
};
function declaredSpyFunction() {
// Dej pozor na to, že proměnná `variableAfterConst` je deklarována později než tato funkce.
console.log(variableAfterConst);
}
const variableAfterConst = "Toto je hodnota proměnné po const";
const functionExpressionAfterConst = function () {
console.log("Tohle stejně neuvidíš");
};
}
console.log(variableAfterVar); // ✅ "Toto je hodnota proměnné po var"
functionDeclaration(); // ✅ "Tohle uvidíš"
declaredSpyFunction(); // ✅ "Toto je hodnota proměnné po const"
console.log(variableAfterConst); // ⛔️ variableAfterConst is not defined
functionExpressionAfterConst();
Code language: JavaScript (javascript)
V paměti zařízení to bude vypadat přibližně takto:
{
"LEX-ENV-STORAGE": {
"lex-env-0": { // LE celého skriptu
"Environment_Record": {}, // prázdné, protože všechny proměnné byly deklarovány v bloku
"Outer_LE_id": null
},
"lex-env-1": { // LE bloku
"Environment_Record": {
"variableAfterConst": "Toto je hodnota proměnné po const",
"functionExpressionAfterConst": {
"type": "function",
"whatToDo": "console.log(\"Tohle stejně neu...\",",
"parameters": [],
"reference_to_outer_LE": "lex-env-1",
"reference_to_outer_VE": "var-env-0"
}
},
"Outer_LE_id": "lex-env-0"
}
},
"VAR-ENV-STORAGE": {
"var-env-0": { // VE celého skriptu
"variableAfterVar": "Toto je hodnota proměnné po var",
"functionDeclaration": {
"type": "function",
"whatToDo": "console.log(\"Tohle uvidíš\");",
"parameters": [],
"reference_to_outer_LE": "lex-env-1", // LE bloku, kde byla deklarována
"reference_to_outer_VE": "var-env-0" // nejbližší VE, který v bloku není, proto VE celého skriptu
},
"declaredSpyFunction": {
"type": "function",
"whatToDo": "console.log(variableAfterConst);",
"parameters": [],
"reference_to_outer_LE": "lex-env-1",
"reference_to_outer_VE": "var-env-0"
}
}
// pro blok VE není a nebude, protože je to prostý blok
}
}Code language: JSON / JSON with Comments (json)
A odpovídajícím způsobem mimo blok console.log(variableAfterConst); vyhodí chybu, ale všechno předtím bude fungovat (ovšem jen ve sloppy mode).
Všimni si: Když zavoláme declaredSpyFunction();, tak chyba nenastane. Funkce declaredSpyFunction se sice ukládá ve variable environment, ale její reference na vnější LE odkazuje na lexical environment bloku, kde byla deklarována. Proto declaredSpyFunction bez problémů získá přístup k variableAfterConst.
Všimni si ještě víc: Pokud v mém příkladu nahoře přidáš "use strict", tak chyba vyskočí už ve fázi functionDeclaration();, protože ve strict mode engine přidává function declaration taky právě do lexical environment, a ne do variable environment.
Ještě nuance o strict mode: Pokud prostě kopíruješ kód do konzole nebo ho připojuješ jako obyčejný .js soubor a jako za starých dobrých časů ho lehce a bez řešení připojíš k html přes <script src="sandbox.js"></script> a explicitně neuvedeš "use strict", tak engine spouští tvůj kód v režimu sloppy.
Ale pokud to uděláš v projektu, kde je nastavený vite / webpack, a napíšeš to do souboru, který pak importuješ jinam, tak s největší pravděpodobností bude tento soubor spuštěn v režimu strict mode. Protože i když explicitně neuvedeš "use strict", často to za tebe udělá bundler. A i když bundler "use strict" nevloží, ale tento soubor se importuje do jiného souboru, už to není jen soubor, ale modul. A moduly prohlížeč automaticky spouští ve strict mode.
Proto až budeš spouštět kód v konzoli a on bude i ve sloppy mode fungovat, jako by tam byl strict mode – ujisti se, že jsi ho spustil opravdu ve sloppy mode, a ne ve strict mode.
A HLAVNĚ: v normálním productionu, dokonce i v relativním legacy (vše mladší 15 let) – všechno je rozbité do samostatných souborů, všechno je rozdělené na spoustu různých oddělených komponent a funkcí, které si navzájem nepřekážejí, všude je strict mode a všechno funguje normálně lidsky. A nikdo nedrží v hlavě celé tohle dinosauří dědictví a nepřemýšlí: „Co se stane, když udělám změnu na řádku 219380, jak to ovlivní kód na řádku 182738.“ Já si sice teď při psaní článku ty nuance vybavil, ale jen tak v běžný pracovní den bez přípravy bych si všechny tyhle drobnosti z paměti nevylovil.
Co je opravdu důležité – to je vštípit si do hlavy mentální model toho, že proměnné nelétají chaoticky v magických obláčcích, ale jsou zcela organizovaně uložené v paměti zařízení. A že funkce je jen kniha s kódem (který vykonává engine, nikoliv funkce samotná). A že k funkci se „přišije“ odkaz na vnější LE, aby se při spuštění kódu dané funkce ten odkaz zkopíroval. Právě to skutečně pomůže rozptýlit magickou mlhu neznáma, lépe debugovat kód a chápat, co je kde uloženo a v jaký okamžik má která proměnná jakou hodnotu.
A u všech těch deprecated věcí typu var, sloppy mode atd. – stačí prostě vědět, že existují. Ale nic víc.
Teď už 100% závěr
Děkuju, pokud jsi to vydržel(a). Budu rád, jestli ti to pomohlo.
Klidně piš otázky do komentářů, pokud ti zůstaly nějaké nejasnosti.
Můžeš se také přidat do mé sítě na LinkedInu. Jsem v Česku relativně nový a budu rád za nová spojení.
A měj se! Ahoj!