Proměnný rozsah, uzavření

JavaScript je velmi funkčně orientovaný jazyk. Dává nám to velkou svobodu. Funkci lze vytvořit kdykoli, předat ji jako argument jiné funkci a poté ji zavolat z úplně jiného místa kódu.

Již víme, že funkce může přistupovat k proměnným mimo ni („vnější“ proměnné).

Co se ale stane, když se vnější proměnné od vytvoření funkce změní? Získá funkce novější nebo staré hodnoty?

A co když je funkce předána jako argument a volána z jiného místa kódu, získá přístup k vnějším proměnným na novém místě?

Rozšiřme své znalosti, abychom porozuměli těmto i složitějším scénářům.

Budeme mluvit o let/const proměnné zde

V JavaScriptu existují 3 způsoby, jak deklarovat proměnnou:let , const (ty moderní) a var (pozůstatek minulosti).

  • V tomto článku budeme používat let proměnné v příkladech.
  • Proměnné deklarované pomocí const , chovají se stejně, takže tento článek je o const taky.
  • Starý var má některé pozoruhodné rozdíly, budou popsány v článku The old "var".

Bloky kódu

Pokud je proměnná deklarována uvnitř bloku kódu {...} , je vidět pouze uvnitř tohoto bloku.

Například:

{
 // do some job with local variables that should not be seen outside

 let message = "Hello"; // only visible in this block

 alert(message); // Hello
}

alert(message); // Error: message is not defined

Můžeme to použít k izolaci části kódu, který dělá svůj vlastní úkol, s proměnnými, které mu patří:

{
 // show message
 let message = "Hello";
 alert(message);
}

{
 // show another message
 let message = "Goodbye";
 alert(message);
}
Bez bloků by došlo k chybě

Vezměte prosím na vědomí, že bez samostatných bloků by došlo k chybě, pokud použijeme let se stávajícím názvem proměnné:

// show message
let message = "Hello";
alert(message);

// show another message
let message = "Goodbye"; // Error: variable already declared
alert(message);

Pro if , for , while a tak dále, proměnné deklarované v {...} jsou také viditelné pouze uvnitř:

if (true) {
 let phrase = "Hello!";

 alert(phrase); // Hello!
}

alert(phrase); // Error, no such variable!

Zde za if končí, alert níže neuvidíte phrase , proto ta chyba.

To je skvělé, protože nám to umožňuje vytvářet blokové lokální proměnné, specifické pro if větev.

Podobná věc platí pro for a while smyčky:

for (let i = 0; i < 3; i++) {
 // the variable i is only visible inside this for
 alert(i); // 0, then 1, then 2
}

alert(i); // Error, no such variable

Vizuálně let i je mimo {...} . Ale for konstrukt je zde speciální:proměnná, deklarovaná uvnitř, je považována za součást bloku.

Vnořené funkce

Funkce se nazývá „vnořená“, když je vytvořena uvnitř jiné funkce.

Je to snadno možné provést pomocí JavaScriptu.

Můžeme jej použít k uspořádání našeho kódu takto:

function sayHiBye(firstName, lastName) {

 // helper nested function to use below
 function getFullName() {
 return firstName + " " + lastName;
 }

 alert( "Hello, " + getFullName() );
 alert( "Bye, " + getFullName() );

}

Zde je vnořeno funkce getFullName() je vyroben pro 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 docela běžné.

A co je mnohem zajímavější, vnořená funkce může být vrácena:buď jako vlastnost nového objektu, nebo jako výsledek sama o sobě. To se pak dá použít někde jinde. Bez ohledu na to, kde, má stále přístup ke stejným vnějším proměnným.

Níže makeCounter vytvoří funkci „počítadla“, která při každém vyvolání vrátí další číslo:

function makeCounter() {
 let count = 0;

 return function() {
 return count++;
 };
}

let counter = makeCounter();

alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2

Přestože jsou jednoduché, mírně upravené varianty tohoto kódu mají praktické využití, například jako generátor náhodných čísel pro generování náhodných hodnot pro automatizované testy.

Jak to funguje? Pokud vytvoříme více čítačů, budou nezávislé? Co se zde děje s proměnnými?

Pochopení takových věcí je skvělé pro celkovou znalost JavaScriptu a přínosné pro složitější scénáře. Pojďme tedy trochu do hloubky.

Lexikální prostředí

Tady jsou draci!

Před námi je podrobné technické vysvětlení.

Pokud bych se rád vyhnul jazykovým detailům na nízké úrovni, jakékoli porozumění bez nich by bylo nedostatečné a neúplné, takže se připravte.

Pro jasnost je vysvětlení rozděleno do několika kroků.

Krok 1. Proměnné

V JavaScriptu každá spuštěná funkce, blok kódu {...} a skript jako celek mají interní (skrytý) přidružený objekt známý jako Lexikální prostředí .

Objekt Lexikální prostředí se skládá ze dvou částí:

  1. Záznam o životním prostředí – objekt, který ukládá všechny lokální proměnné jako své vlastnosti (a některé další informace, jako je hodnota this ).
  2. Odkaz na vnější lexikální prostředí , která je spojena s vnějším kódem.

Proměnná je pouze vlastnost speciálního interního objektu Environment Record . „Získat nebo změnit proměnnou“ znamená „získat nebo změnit vlastnost daného objektu“.

V tomto jednoduchém kódu bez funkcí je pouze jedno Lexikální prostředí:

Jedná se o tzv. globální Lexikální prostředí, spojené s celým scénářem.

Na obrázku výše obdélník znamená Záznam prostředí (variabilní úložiště) a šipka znamená vnější referenci. Globální lexikální prostředí nemá žádnou vnější referenci, proto šipka ukazuje na null .

Jak se kód začíná spouštět a pokračuje, Lexikální prostředí se mění.

Zde je trochu delší kód:

Obdélníky na pravé straně ukazují, jak se mění globální lexikální prostředí během provádění:

  1. Když se skript spustí, Lexikální prostředí se předvyplní všemi deklarovanými proměnnými.
    • Zpočátku jsou ve stavu „Neinicializováno“. Toto je speciální interní stav, což znamená, že modul ví o proměnné, ale nelze na ni odkazovat, dokud není deklarována pomocí let . Je to téměř stejné, jako kdyby proměnná neexistovala.
  2. Poté let phrase objeví se definice. Zatím neexistuje žádné přiřazení, takže jeho hodnota je undefined . Od tohoto okamžiku můžeme proměnnou používat.
  3. phrase je přiřazena hodnota.
  4. phrase změní hodnotu.

Všechno zatím vypadá jednoduše, že?

  • Proměnná je vlastnost speciálního interního objektu přidruženého k aktuálně prováděnému bloku/funkci/skriptu.
  • Práce s proměnnými je ve skutečnosti práce s vlastnostmi daného objektu.
Lexikální prostředí je objekt specifikace

„Lexikální prostředí“ je objekt specifikace:existuje pouze „teoreticky“ v jazykové specifikaci, aby popsal, jak věci fungují. Nemůžeme tento objekt dostat do našeho kódu a přímo s ním manipulovat.

JavaScriptové enginy jej také mohou optimalizovat, vyřadit proměnné, které se nepoužívají k šetření paměti a provádět další interní triky, pokud viditelné chování zůstane popsané.

Krok 2. Deklarace funkcí

Funkce je také hodnota, jako proměnná.

Rozdíl je v tom, že deklarace funkce je okamžitě plně inicializována.

Když je vytvořeno lexikální prostředí, deklarace funkce se okamžitě stává funkcí připravenou k použití (na rozdíl od let , která je do deklarace nepoužitelná).

Proto můžeme použít funkci, deklarovanou jako Function Declaration, ještě před samotnou deklarací.

Zde je například počáteční stav globálního lexikálního prostředí, když přidáme funkci:

Toto chování se přirozeně vztahuje pouze na deklarace funkcí, nikoli na výrazy funkcí, kde přiřazujeme funkci proměnné, například let say = function(name)... .

Krok 3. Vnitřní a vnější lexikální prostředí

Když je funkce spuštěna, na začátku volání se automaticky vytvoří nové Lexikální prostředí pro uložení lokálních proměnných a parametrů volání.

Například pro say("John") , vypadá to takto (provedení 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 provedení say . Má jedinou vlastnost:name , argument funkce. Zavolali jsme say("John") , tedy hodnotu name je "John" .
  • Vnější lexikální prostředí je globální lexikální prostředí. Má phrase proměnná a samotná funkce.

Vnitřní lexikální prostředí má odkaz na outer jeden.

Když chce kód přistupovat k proměnné – nejprve se prohledá vnitřní lexikální prostředí, poté vnější, pak vnější a tak dále až do globálního.

Pokud proměnná není nikde nalezena, jedná se o chybu v přísném režimu (bez use strict , přiřazení k neexistující proměnné vytvoří novou globální proměnnou pro kompatibilitu se starým kódem).

V tomto příkladu vyhledávání probíhá následovně:

  • Pro name proměnná alert uvnitř say najde ji okamžitě ve vnitřním lexikálním prostředí.
  • Když chce získat přístup k phrase , pak neexistuje žádný phrase lokálně, takže následuje odkaz na vnější lexikální prostředí a najde ho tam.

Krok 4. Vrácení funkce

Vraťme se k makeCounter příklad.

function makeCounter() {
 let count = 0;

 return function() {
 return count++;
 };
}

let counter = makeCounter();

Na začátku každého makeCounter() zavolání, je vytvořen nový objekt Lexical Environment, do kterého budou uloženy proměnné pro tento makeCounter spustit.

Máme tedy dvě vnořená lexikální prostředí, stejně jako ve výše uvedeném příkladu:

Liší se tím, že při provádění makeCounter() , je vytvořena malá vnořená funkce pouze z jednoho řádku:return count++ . Zatím to nespouštíme, pouze vytváříme.

Všechny funkce si pamatují Lexikální prostředí, ve kterém byly vytvořeny. Technicky zde není žádná magie:všechny funkce mají skrytou vlastnost s názvem [[Environment]] , který zachovává odkaz na Lexikální prostředí, kde byla funkce vytvořena:

Takže counter.[[Environment]] má odkaz na {count: 0} Lexikální prostředí. Funkce si tak pamatuje, kde byla vytvořena, bez ohledu na to, kde je volána. [[Environment]] reference je nastavena jednou a navždy při vytvoření funkce.

Později, když counter() je zavoláno, je pro volání vytvořeno nové lexikální prostředí a jeho vnější odkaz na lexikální prostředí je převzat z counter.[[Environment]] :

Nyní, když je kód uvnitř counter() hledá count prohledá nejprve své vlastní lexikální prostředí (prázdné, protože tam nejsou žádné lokální proměnné), poté lexikální prostředí vnějšího makeCounter() volání, kde jej najde a změní.

Proměnná je aktualizována v lexikálním prostředí, kde žije.

Zde je stav po provedení:

Pokud zavoláme counter() vícekrát, count proměnná bude zvýšena na 2 , 3 a tak dále, na stejném místě.

Uzavření

Existuje obecný programátorský termín „uzavření“, 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á k nim přístup. V některých jazycích to není možné, nebo by funkce měla být napsána speciálním způsobem, aby se to stalo. Ale jak je vysvětleno výše, v JavaScriptu jsou všechny funkce přirozeně uzavřené (existuje pouze jedna výjimka, kterou pokryje syntaxe "nové funkce").

To znamená:automaticky si pamatují, kde byly vytvořeny pomocí skrytého [[Environment]] a jejich kód pak může přistupovat k vnějším proměnným.

Když na pohovoru dostane vývojář frontendu otázku „co je uzavření?“, platnou odpovědí by byla definice uzavření a vysvětlení, že všechny funkce v JavaScriptu jsou uzavření, a možná ještě pár slov o technických detailech:[[Environment]] vlastnost a jak fungují lexikální prostředí.

Sběr odpadu

Obvykle je Lexikální prostředí odstraněno z paměti se všemi proměnnými po dokončení volání funkce. To proto, že na to nejsou žádné odkazy. Jako každý objekt JavaScriptu je uchováván v paměti pouze tehdy, když je dosažitelný.

Pokud však existuje vnořená funkce, která je stále dosažitelná po skončení funkce, pak má [[Environment]] vlastnost, která odkazuje na lexikální prostředí.

V takovém případě je Lexikální prostředí stále dosažitelné i po dokončení funkce, takže zůstává živé.

Například:

function f() {
 let value = 123;

 return function() {
 alert(value);
 }
}

let g = f(); // g.[[Environment]] stores a reference to the Lexical Environment
// of the corresponding f() call

Upozorňujeme, že pokud f() je volána mnohokrát a výsledné funkce jsou uloženy, pak budou v paměti zachovány také všechny odpovídající objekty Lexikálního prostředí. V níže uvedeném kódu jsou všechny 3:

function f() {
 let value = Math.random();

 return function() { alert(value); };
}

// 3 functions in array, every one of them links to Lexical Environment
// from the corresponding f() run
let arr = [f(), f(), f()];

Objekt Lexikálního prostředí zemře, když se stane nedosažitelným (stejně jako jakýkoli jiný objekt). Jinými slovy, existuje pouze tehdy, když na něj odkazuje alespoň jedna vnořená funkce.

V níže uvedeném kódu je po odstranění vnořené funkce její lexikální prostředí (a tedy value ) je vyčištěn z paměti:

function f() {
 let value = 123;

 return function() {
 alert(value);
 }
}

let g = f(); // while g function exists, the value stays in memory

g = null; // ...and now the memory is cleaned up

Optimalizace v reálném životě

Jak jsme viděli, teoreticky, když je funkce naživu, všechny vnější proměnné jsou také zachovány.

Ale v praxi se to JavaScriptové motory snaží optimalizovat. Analyzují použití proměnné a pokud je z kódu zřejmé, že vnější proměnná není použita, je odstraněna.

Důležitým vedlejším efektem ve V8 (Chrome, Edge, Opera) je, že taková proměnná nebude při ladění dostupná.

Zkuste níže uvedený příklad spustit v prohlížeči Chrome s otevřenými nástroji pro vývojáře.

Když se pozastaví, zadejte v konzole alert(value) .

function f() {
 let value = Math.random();

 function g() {
 debugger; // in console: type alert(value); No such variable!
 }

 return g;
}

let g = f();
g();

Jak jste mohli vidět – žádná taková proměnná neexistuje! Teoreticky by měl být přístupný, ale engine to optimalizoval.

To může vést k zábavným (ne-li tak časově náročným) problémům s laděním. Jeden z nich – můžeme vidět stejnojmennou vnější proměnnou místo očekávané:

let value = "Surprise!";

function f() {
 let value = "the closest value";

 function g() {
 debugger; // in console: type alert(value); Surprise!
 }

 return g;
}

let g = f();
g();

Tuto vlastnost V8 je dobré znát. Pokud ladíte s Chrome/Edge/Operou, dříve nebo později se s tím setkáte.

To není chyba v debuggeru, ale spíše zvláštní vlastnost V8. Snad se to někdy změní. Vždy si to můžete ověřit spuštěním příkladů na této stránce.