JavaScript je znaÄnÄ funkcionálnÄ orientovaný jazyk. Dává nám velké množstvà svobody. Funkci můžeme kdykoli vytvoÅit, pÅedat ji jako argument jiné funkci a pozdÄji ji volat z úplnÄ jiného mÃsta kódu.
VÃme již, že funkce může pÅistupovat k promÄnným, které ležà mimo ni (k âvnÄjÅ¡Ãmâ promÄnným).
Co se vÅ¡ak stane, když se po vytvoÅenà funkce vnÄjšà promÄnné zmÄnÃ? Dostanou se do funkce jejich nové, nebo staré hodnoty?
A co když je funkce pÅedána jako argument a pak je volána z jiného mÃsta kódu? Bude mÃt na novém mÃstÄ pÅÃstup k vnÄjÅ¡Ãm promÄnným?
RozÅ¡iÅme si znalosti, abychom porozumÄli tÄmto i složitÄjÅ¡Ãm scénáÅům.
let/constV JavaScriptu jsou tÅi způsoby, jak deklarovat promÄnnou: let, const (tyto dva jsou modernÃ) a var (pozůstatek minulosti).
- V tomto Älánku budeme v pÅÃkladech použÃvat promÄnné deklarované pomocÃ
let. - PromÄnné deklarované pomocÃ
constse chovajà stejnÄ, takže tento Älánek platà i proconst. - Starý pÅÃkaz
varmá urÄité významné rozdÃly, které probereme v Älánku Starý pÅÃkaz âvarâ.
Kódové bloky
Jestliže je promÄnná deklarována uvnitÅ kódového bloku {...}, je viditelná jedinÄ uvnitÅ tohoto bloku.
PÅÃklad:
{
// provedeme nÄjakou práci s lokálnÃmi promÄnnými, které nemajà být vidÄt zvenÄÃ
let zpráva = "Ahoj"; // je viditelná jen v tomto bloku
alert(zpráva); // Ahoj
}
alert(zpráva); // Chyba: zpráva nenà definována
DÃky tomu můžeme izolovat Äást kódu, která odvede svou vlastnà práci, s promÄnnými, které budou patÅit pouze jÃ:
{
// zobrazà zprávu
let zpráva = "Ahoj";
alert(zpráva);
}
{
// zobrazà jinou zprávu
let zpráva = "Na shledanou";
alert(zpráva);
}
ProsÃme vÅ¡imnÄte si, že bez oddÄlených bloků by nastala chyba, kdybychom použili let s již existujÃcÃm názvem promÄnné:
// zobrazà zprávu
let zpráva = "Ahoj";
alert(zpráva);
// zobrazà jinou zprávu
let zpráva = "Na shledanou"; // Chyba: promÄnná je již deklarována
alert(zpráva);
Také pro if, for, while a podobné jsou promÄnné deklarované v {...} viditelné jedinÄ uvnitÅ:
if (true) {
let vÄta = "Ahoj!";
alert(vÄta); // Ahoj!
}
alert(vÄta); // Chyba, taková promÄnná neexistuje!
Zde po skonÄenà if funkce alert pod nÃm neuvidà promÄnnou vÄta, takže nastane chyba.
To je skvÄlé, protože nám to umožÅuje vytváÅet blokovÄ lokálnà promÄnné, specifické pro vÄtev if.
PodobnÄ to platà pro cykly for a while:
for (let i = 0; i < 3; i++) {
// promÄnná i je viditelná jen uvnitÅ tohoto cyklu for
alert(i); // 0, pak 1, pak 2
}
alert(i); // Chyba, taková promÄnná neexistuje
VizuálnÄ je let i mimo {...}, avÅ¡ak konstrukt for je v tomto ohledu speciálnÃ: promÄnná, která je deklarována uvnitÅ nÄj, se považuje za souÄást bloku.
VnoÅené funkce
Funkce se nazývá âvnoÅenáâ, když je vytvoÅena uvnitÅ jiné funkce.
V JavaScriptu je to snadno možné.
Můžeme to využÃt k organizaci naÅ¡eho kódu, napÅÃklad takto:
function ÅekniAhojNashle(kÅestnÃJméno, pÅÃjmenÃ) {
// pomocná vnoÅená funkce, kterou použijeme nÞe
function vraťCeléJméno() {
return kÅestnÃJméno + " " + pÅÃjmenÃ;
}
alert( "Ahoj, " + vraťCeléJméno() );
alert( "Nashle, " + vraťCeléJméno() );
}
Zde je vnoÅená funkce vraÅ¥CeléJméno() vytvoÅena pro naÅ¡e pohodlÃ. Může pÅistupovat k vnÄjÅ¡Ãm promÄnným, a tak může vrátit celé jméno. VnoÅené funkce jsou v JavaScriptu pomÄrnÄ bÄžné.
JeÅ¡tÄ zajÃmavÄjšà je, že vnoÅenou funkci můžeme vrátit: buÄ jako vlastnost nového objektu, nebo jako samotný výsledek funkce. Pak ji můžeme použÃt nÄkde jinde. AÅ¥ to bude kdekoli, stále bude mÃt pÅÃstup ke stejným vnÄjÅ¡Ãm promÄnným.
V následujÃcÃm pÅÃkladu vytvoÅÄÃtaÄ vytvoÅà funkci âÄÃtaÄeâ, která pÅi každém zavolánà vrátà dalšà ÄÃslo:
function vytvoÅÄÃtaÄ() {
let poÄet = 0;
return function() {
return poÄet++;
};
}
let ÄÃtaÄ = vytvoÅÄÃtaÄ();
alert( ÄÃtaÄ() ); // 0
alert( ÄÃtaÄ() ); // 1
alert( ÄÃtaÄ() ); // 2
MÃrnÄ upravené varianty tohoto kódu, tÅebaže jsou jednoduché, majà praktické využitÃ, napÅÃklad jako generátor pseudonáhodných ÄÃsel, který generuje náhodné hodnoty pro automatizované testy.
Jak to funguje? Když vytvoÅÃme vÃce ÄÃtaÄů, budou nezávislé? Co se bude dÃt se zdejÅ¡Ãmi promÄnnými?
PorozumÄt takovým vÄcem je skvÄlé pro vÅ¡eobecnou znalost JavaScriptu a vyplatà se pÅi složitÄjÅ¡Ãch scénáÅÃch. PojÄme tedy trochu do hloubky.
Lexikálnà prostÅedÃ
PÅed námi ležà hlubšà technické vysvÄtlenÃ.
Jakkoli se snažÃm vyhnout se nÃzkoúrovÅovým detailům jazyka, bez nich by porozumÄnà bylo dÄravé a neúplné, takže se na nÄ pÅipravte.
Aby to bylo jasnÄjÅ¡Ã, rozdÄlÃme vysvÄtlenà na nÄkolik kroků.
Krok 1. PromÄnné
V JavaScriptu je ke každé spuÅ¡tÄné funkci, kódovému bloku {...} i celému skriptu pÅipojen internà (skrytý) objekt, nazývaný lexikálnà prostÅedÃ.
Objekt lexikálnÃho prostÅedà se skládá ze dvou ÄástÃ:
- Záznam prostÅedà â objekt, v nÄmž jsou uloženy vÅ¡echny lokálnà promÄnné jako jeho vlastnosti (a nÄkteré dalšà informace, napÅ. hodnota
this). - Odkaz na vnÄjšà lexikálnà prostÅedÃ, tedy to, které je spojeno s vnÄjÅ¡Ãm kódem.
âPromÄnnáâ je jen vlastnost speciálnÃho internÃho objektu, záznamu prostÅedÃ. âNaÄÃst nebo zmÄnit promÄnnouâ znamená ânaÄÃst nebo zmÄnit vlastnost tohoto objektuâ.
V tomto jednoduchém kódu bez funkcà existuje pouze jedno lexikálnà prostÅedÃ:
To je tzv. globálnà lexikálnà prostÅedÃ, pÅipojené k celému skriptu.
ObdélnÃk v uvedeném obrázku znamená záznam prostÅedà (skladiÅ¡tÄ promÄnných) a Å¡ipka znamená odkaz na vnÄjšà prostÅedÃ. Globálnà lexikálnà prostÅedà nemá žádný odkaz na vnÄjšà prostÅedÃ, proto Å¡ipka ukazuje na null.
Když se kód zaÄne provádÄt, lexikálnà prostÅedà se s jeho bÄhem mÄnÃ.
Zde je trochu delšà kód:
ObdélnÃky napravo ukazujÃ, jak se globálnà lexikálnà prostÅedà mÄnà bÄhem provádÄnà kódu:
- Když se skript spustÃ, lexikálnà prostÅedà se obsadà vÅ¡emi deklarovanými promÄnnými.
- Na zaÄátku jsou ve stavu âneinicializovánoâ. To je speciálnà vnitÅnà stav, který znamená, že motor và o promÄnné, ale nelze se na ni odkazovat, dokud nebude deklarována pomocÃ
let. Je to skoro totéž, jako by promÄnná neexistovala.
- Na zaÄátku jsou ve stavu âneinicializovánoâ. To je speciálnà vnitÅnà stav, který znamená, že motor và o promÄnné, ale nelze se na ni odkazovat, dokud nebude deklarována pomocÃ
- Pak se objevà definice
let vÄta. ZatÃm zde nenà žádné pÅiÅazenÃ, takže hodnota promÄnné jeundefined. Od této chvÃle můžeme tuto promÄnnou použÃvat. - Do promÄnné
vÄtaje pÅiÅazena hodnota. - Hodnota promÄnné
vÄtase zmÄnÃ.
ProzatÃm to vÅ¡echno vypadá jednoduÅ¡e, že?
- PromÄnná je vlastnostà speciálnÃho internÃho objektu, pÅipojeného k právÄ vykonávanému bloku/funkci/skriptu.
- Práce s promÄnnými je ve skuteÄnosti práce s vlastnostmi tohoto objektu.
âLexikálnà prostÅedÃâ je objekt ze specifikace: existuje jen âteoretickyâ ve specifikaci jazyka, aby popisoval, jak vÅ¡e funguje. V naÅ¡em kódu nemůžeme tento objekt zÃskat a pÅÃmo s nÃm manipulovat.
Motory JavaScriptu jej také mohou optimalizovat, vyÅazovat nepoužÃvané promÄnné, aby uÅ¡etÅily pamÄÅ¥, a provádÄt jiné vnitÅnà triky, pokud jeho viditelné chovánà zůstává takové, jak je zde popsáno.
Krok 2. Deklarace funkcÃ
Funkce je také hodnota, stejnÄ jako promÄnná.
RozdÃl je v tom, že pÅi deklaraci je funkce okamžitÄ plnÄ inicializována.
Když je vytvoÅeno lexikálnà prostÅedÃ, deklarace funkce okamžitÄ vytvoÅà funkci pÅipravenou k použità (na rozdÃl od promÄnné v let, která je pÅed deklaracà nepoužitelná).
Z tohoto důvodu můžeme použÃvat funkci, deklarovanou deklaracà funkce, jeÅ¡tÄ pÅed samotnou deklaracÃ.
NapÅÃklad zde je úvodnà stav globálnÃho lexikálnÃho prostÅedÃ, když pÅidáme funkci:
PochopitelnÄ toto chovánà platà jen pro deklarace funkcÃ, ne pro funkÄnà výrazy, v nichž pÅiÅazujeme funkci do promÄnné, napÅÃklad let Åekni = function(jméno)....
Krok 3. VnitÅnà a vnÄjšà lexikálnà prostÅedÃ
Když se spustà funkce, na zaÄátku jejÃho volánà je automaticky vytvoÅeno nové lexikálnà prostÅedÃ, do nÄhož se ukládajà lokálnà promÄnné a parametry volánÃ.
NapÅÃklad pro Åekni("Jan") vypadá takto (bÄh je na Åádku oznaÄeném Å¡ipkou):
BÄhem volánà funkce máme dvÄ lexikálnà prostÅedÃ: vnitÅnà (pro volánà funkce) a vnÄjšà (globálnÃ):
- VnitÅnà lexikálnà prostÅedà odpovÃdá aktuálnÃmu bÄhu funkce
Åekni. Má jedinou vlastnost:jméno, argument funkce. Voláme jiÅekni("Jan"), takže hodnota vlastnostijménoje"Jan". - VnÄjšà lexikálnà prostÅedà je globálnà lexikálnà prostÅedÃ. Má promÄnnou
vÄtaa samotnou funkci.
VnitÅnà lexikálnà prostÅedà obsahuje odkaz outer na vnÄjÅ¡Ã.
Když kód chce pÅistupovat k promÄnné â nejprve se prohledá vnitÅnà lexikálnà prostÅedÃ, pak vnÄjÅ¡Ã, pak jeÅ¡tÄ vnÄjšà a tak dále, až ke globálnÃmu.
NenÃ-li promÄnná nikde nalezena, ve striktnÃm režimu nastane chyba (bez use strict pÅiÅazenà do neexistujÃcà promÄnné vytvoÅà novou globálnà promÄnnou, aby byla zachována kompatibilita se starým kódem).
V tomto pÅÃkladu hledánà postupuje následovnÄ:
- Co se týÄe promÄnné
jméno,alertuvnitÅÅekniji najde okamžitÄ ve vnitÅnÃm lexikálnÃm prostÅedÃ. - Když chce pÅistupovat k promÄnné
vÄta, pak lokálnÄ Å¾Ã¡dnou promÄnnouvÄtanenajde, takže pokraÄuje odkazem na vnÄjšà lexikálnà prostÅedà a najde ji v nÄm.
Krok 4. Vrácenà funkce
VraÅ¥me se k pÅÃkladu vytvoÅÄÃtaÄ.
function vytvoÅÄÃtaÄ() {
let poÄet = 0;
return function() {
return poÄet++;
};
}
let ÄÃtaÄ = vytvoÅÄÃtaÄ();
Na zaÄátku každého volánà vytvoÅÄÃtaÄ() se vytvoÅà nový objekt lexikálnÃho prostÅedÃ, do nÄhož se uložà promÄnné pro tento bÄh funkce vytvoÅÄÃtaÄ.
Máme tedy dvÄ vnoÅená lexikálnà prostÅedÃ, podobnÄ jako ve výše uvedeném pÅÃkladu:
RozdÃl spoÄÃvá v tom, že bÄhem provádÄnà funkce vytvoÅÄÃtaÄ() se vytvoÅà drobná vnoÅená funkce tvoÅená jediným Åádkem: return poÄet++. Tuto funkci zatÃm nevoláme, jenom ji vytvoÅÃme.
VÅ¡echny funkce si pamatujà lexikálnà prostÅedÃ, v nÄmž byly vytvoÅeny. Technicky v tom nenà nic magického: vÅ¡echny funkce majà skrytou vlastnost jménem [[Environment]], která si udržuje odkaz na lexikálnà prostÅedÃ, v nÄmž byla funkce vytvoÅena:
Takže ÄÃtaÄ.[[Environment]] má odkaz na lexikálnà prostÅedà {poÄet: 0}. TÃmto způsobem si funkce pamatuje, kde byla vytvoÅena, bez ohledu na to, kde je volána. Odkaz [[Environment]] se pÅi vytvoÅenà funkce nastavà jednou provždy.
Když je ÄÃtaÄ() pozdÄji volán, vytvoÅà se pro toto volánà nové lexikálnà prostÅedà a odkaz na jeho vnÄjšà lexikálnà prostÅedà se pÅevezme z ÄÃtaÄ.[[Environment]]:
Když nynà kód uvnitÅ funkce ÄÃtaÄ() hledá promÄnnou poÄet, nejprve prohledá své vlastnà lexikálnà prostÅedà (prázdné, jelikož tady nejsou žádné lokálnà promÄnné), pak lexikálnà prostÅedà vnÄjÅ¡Ãho volánà vytvoÅÄÃtaÄ(), kde ji najde a zmÄnÃ.
PromÄnná je zmÄnÄna v lexikálnÃm prostÅedÃ, v nÄmž pÅebývá.
Zde je stav po provedenà funkce:
Jestliže voláme ÄÃtaÄ() vÃcekrát, promÄnná poÄet se zvýšà na 2, 3 a tak dále, a to na stejném mÃstÄ.
Existuje obecný programovacà pojem âuzávÄrâ, který by vývojáÅi obecnÄ mÄli znát.
UzávÄr je funkce, která si pamatuje své vnÄjšà promÄnné a může k nim pÅistupovat. V nÄkterých jazycÃch to nenà možné nebo funkce musà být napsána speciálnÃm způsobem, aby se to mohlo dÃt. Ale jak bylo vysvÄtleno výše, v JavaScriptu jsou vÅ¡echny funkce pÅirozenÄ uzávÄry (je tady jen jedna výjimka, kterou probereme v kapitole Syntaxe ânew Functionâ).
To znamená: pomocà skryté vlastnosti [[Environment]] si automaticky pamatujÃ, kde byly vytvoÅeny, a jejich kód pak může pÅistupovat k vnÄjÅ¡Ãm promÄnným.
Kdyby front-end vývojáŠv rozhovoru dostal otázku âco je to uzávÄr?â, správná odpovÄÄ by byla definice uzávÄru a vysvÄtlenÃ, že v JavaScriptu jsou vÅ¡echny funkce uzávÄry, a možná nÄkolik dalÅ¡Ãch slov o technických detailech: o vlastnosti [[Environment]] a o tom, jak fungujà lexikálnà prostÅedÃ.
SbÄr odpadků
Lexikálnà prostÅedà funkce je zpravidla odstranÄno z pamÄti i se vÅ¡emi promÄnnými poté, co volánà funkce skonÄÃ. Je to proto, že pak už na nÄ neexistujà žádné odkazy. StejnÄ jako vÅ¡echny objekty v JavaScriptu je udržováno v pamÄti, jen dokud je dosažitelné.
Jestliže vÅ¡ak existuje vnoÅená funkce, která je po skonÄenà funkce stále dosažitelná, pak tato funkce má vlastnost [[Environment]], která se na toto lexikálnà prostÅedà odkazuje.
V takovém pÅÃpadÄ je lexikálnà prostÅedà stále dostupné i po skonÄenà funkce, takže bude nadále existovat.
NapÅÃklad:
function f() {
let hodnota = 123;
return function() {
alert(hodnota);
}
}
let g = f(); // g.[[Environment]] si uložà odkaz na lexikálnà prostÅedÃ
// pÅÃsluÅ¡ného volánà f()
ProsÃme vÅ¡imnÄte si, že je-li f() volána mnohokrát a výsledné funkce jsou nÄkam uloženy, pak zůstanou v pamÄti i vÅ¡echny pÅÃsluÅ¡né objekty lexikálnÃch prostÅedÃ. V následujÃcÃm kódu to budou vÅ¡echny tÅi:
function f() {
let hodnota = Math.random();
return function() { alert(hodnota); };
}
// 3 funkce v poli, každá z nich se odkazuje na lexikálnà prostÅedÃ
// z pÅÃsluÅ¡ného spuÅ¡tÄnà f()
let pole = [f(), f(), f()];
Objekt lexikálnÃho prostÅedà je zniÄen, když se stane nedosažitelným (stejnÄ jako každý jiný objekt). Jinými slovy, bude existovat, jen dokud bude existovat nejménÄ jedna vnoÅená funkce, která se na nÄj odkazuje.
V následujÃcÃm kódu je po odstranÄnà vnoÅené funkce jejà uzavÃrajÃcà lexikálnà prostÅedà (a tedy i hodnota) vymazáno z pamÄti:
function f() {
let hodnota = 123;
return function() {
alert(hodnota);
}
}
let g = f(); // dokud existuje funkce g, hodnota zůstane v pamÄti
g = null; // ...a nynà bude pamÄÅ¥ proÄiÅ¡tÄna
Optimalizace v reálném životÄ
Jak jsme vidÄli, teoreticky dokud funkce existuje, jsou udržovány i vÅ¡echny vnÄjšà promÄnné.
V praxi se vÅ¡ak JavaScriptové motory snažà o optimalizaci. Analyzujà použÃvánà promÄnných, a je-li z kódu zÅejmé, že vnÄjšà promÄnná nenà nikde použita, bude odstranÄna.
Důležitý vedlejšà efekt ve V8 (Chrome, Edge, Opera) je, že taková promÄnná pÅestane být dostupná pÅi ladÄnÃ.
Zkuste si spustit nÞe uvedený pÅÃklad v Chrome s otevÅenými vývojáÅskými nástroji.
Když se zastavÃ, v konzoli zadejte alert(hodnota).
function f() {
let hodnota = Math.random();
function g() {
debugger; // v konzoli zadejte: alert(hodnota); taková promÄnná neexistuje!
}
return g;
}
let g = f();
g();
Jak vidÃme â taková promÄnná neexistuje! Teoreticky by mÄla být dostupná, ale motor ji vyÅadil pÅi optimalizaci.
To může vést k zábavným (kdyby nezabÃraly tolik Äasu) problémům pÅi ladÄnÃ. Jeden z nich â můžeme vidÄt vnÄjšà promÄnnou se stejným názvem namÃsto oÄekávané:
let hodnota = "PÅekvapenÃ!";
function f() {
let hodnota = "nejbližšà hodnota";
function g() {
debugger; // v konzoli zadejte: alert(hodnota); PÅekvapenÃ!
}
return g;
}
let g = f();
g();
Tuto vlastnost V8 je dobré znát. Jestliže ladÃte v Chrome, Edge nebo OpeÅe, dÅÃve nebo pozdÄji se s nà setkáte.
Nenà to chyba ladicÃho nástroje, ale spÃÅ¡e speciálnà vlastnost V8. Možná bude Äasem zmÄnÄna. Vždy si ji můžete ovÄÅit spuÅ¡tÄnÃm pÅÃkladů na této stránce.
KomentáÅe
<code>, pro nÄkolik Åádků je obalte znaÄkou<pre>, pro vÃce než 10 Åádků vložte odkaz na pÃskoviÅ¡tÄ (plnkr, jsbin, codepenâ¦)