4. února 2025

Oblast platnosti proměnné, uzávěr

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.

Zde budeme hovořit o proměnných deklarovaných pomocí let/const

V 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í const se chovají stejně, takže tento článek platí i pro const.
  • Starý příkaz var má 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);
}
Bez bloků by nastala chyba

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í

Zde jsou draci!

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í:

  1. 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).
  2. 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:

  1. 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.
  2. Pak se objeví definice let věta. Zatím zde není žádné přiřazení, takže hodnota proměnné je undefined. Od této chvíle můžeme tuto proměnnou používat.
  3. Do proměnné věta je přiřazena hodnota.
  4. Hodnota proměnné věta se 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

„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 vlastnosti jméno je "Jan".
  • Vnější lexikální prostředí je globální lexikální prostředí. Má proměnnou věta a 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, alert uvnitř řekni ji 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ěnnou věta nenajde, 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ě.

Uzávěr

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.

Úlohy

důležitost: 5

Funkce řekniAhoj využívá název externí proměnné. Když bude spuštěna, kterou hodnotu použije?

let jméno = "Jan";

function řekniAhoj() {
  alert("Ahoj, " + jméno);
}

jméno = "Petr";

řekniAhoj(); // co zobrazí: "Jan" nebo "Petr"?

Takové situace jsou běžné při vývoji v prohlížeči i na straně serveru. Funkce může být navržena ke spuštění později, než byla vytvořena, například po uživatelské akci nebo síťovém požadavku.

Otázka tedy zní: odráží poslední změny?

Odpověď zní: Petr.

Funkce načítá vnější proměnné tak, jak vypadají právě v tuto chvíli. Používá poslední hodnoty.

Staré hodnoty proměnných se nikam neukládají. Když funkce chce proměnnou, vezme její aktuální hodnotu ze svého vlastního lexikálního prostředí nebo z vnějšího.

důležitost: 5

Následující funkce vytvořPracovníka vytvoří jinou funkci a vrátí ji. Nová funkce může být volána odjinud.

Bude mít přístup k vnějším proměnným z místa svého vzniku, nebo z místa volání, nebo z obojího?

function vytvořPracovníka() {
  let jméno = "Petr";

  return function() {
    alert(jméno);
  };
}

let jméno = "Jan";

// vytvoření funkce
let pracuj = vytvořPracovníka();

// její volání
pracuj(); // co zobrazí?

Kterou hodnotu zobrazí? „Petr“ nebo „Jan“?

Odpověď zní: Petr.

Funkce pracuj() uvedená v následujícím kódu načte jméno z místa svého vzniku odkazem na vnější lexikální prostředí:

Výsledkem zde je tedy "Petr".

Kdyby však ve funkci vytvořPracovníka() nebylo let jméno, pak by hledání pokračovalo dál ven a převzalo globální proměnnou, jak vidíme v uvedeném řetězci. V tom případě by výsledek byl "Jan".

důležitost: 5

Zde vytvoříme dva čítače: čítač a čítač2 pomocí téže funkce vytvořČítač.

Jsou nezávislé? Co zobrazí druhý čítač? 0,1 nebo 2,3 nebo něco jiného?

function vytvořČítač() {
  let počet = 0;

  return function() {
    return počet++;
  };
}

let čítač = vytvořČítač();
let čítač2 = vytvořČítač();

alert( čítač() ); // 0
alert( čítač() ); // 1

alert( čítač2() ); // ?
alert( čítač2() ); // ?

Odpověď: 0,1.

Funkce čítač a čítač2 byly vytvořeny různými voláními funkce vytvořČítač.

Mají tedy nezávislá vnější lexikální prostředí, každé z nich má svůj vlastní počet.

důležitost: 5

Zde je vytvořen objekt čítače pomocí konstruktoru.

Bude to fungovat? Co se zobrazí?

function Čítač() {
  let počet = 0;

  this.zvyš = function() {
    return ++počet;
  };
  this.sniž = function() {
    return --počet;
  };
}

let čítač = new Čítač();

alert( čítač.zvyš() ); // ?
alert( čítač.zvyš() ); // ?
alert( čítač.sniž() ); // ?

Jistě že to bude fungovat správně.

Obě vnořené funkce jsou vytvořeny uvnitř stejného vnějšího lexikálního prostředí, takže mají společný přístup ke stejné proměnné počet:

function Čítač() {
  let počet = 0;

  this.zvyš = function() {
    return ++počet;
  };
  this.sniž = function() {
    return --počet;
  };
}

let čítač = new Čítač();

alert( čítač.zvyš() ); // 1
alert( čítač.zvyš() ); // 2
alert( čítač.sniž() ); // 1
důležitost: 5

Podívejte se na kód. Jaký bude výsledek volání na posledním řádku?

let věta = "Ahoj";

if (true) {
  let uživatel = "Jan";

  function řekniAhoj() {
    alert(`${věta}, ${uživatel}`);
  }
}

řekniAhoj();

Výsledkem bude chyba.

Funkce řekniAhoj je deklarována uvnitř if, takže bude existovat jen uvnitř něj. Vně žádné řekniAhoj není.

důležitost: 4

Napište funkci sečti, která bude fungovat takto: sečti(a)(b) = a+b.

Ano, přesně takto, pomocí dvojích závorek (to není překlep).

Například:

sečti(1)(2) = 3
sečti(5)(-1) = 4

Aby druhé závorky fungovaly, ty první musejí vrátit funkci.

Například:

function sečti(a) {

  return function(b) {
    return a + b; // vezme "a" z vnějšího lexikálního prostředí
  };

}

alert( sečti(1)(2) ); // 3
alert( sečti(5)(-1) ); // 4
důležitost: 4

Jaký bude výsledek tohoto kódu?

let x = 1;

function funkce() {
  console.log(x); // ?

  let x = 2;
}

funkce();

P.S. V tomto úkolu je chyták. Řešení není očividné.

Výsledek bude: chyba.

Zkuste si to spustit:

let x = 1;

function funkce() {
  console.log(x); // ReferenceError: Nelze přistupovat k 'x' před inicializací
  let x = 2;
}

funkce();

V tomto příkladu můžeme vidět pozoruhodný rozdíl mezi „neexistující“ a „neinicializovanou“ proměnnou.

Jak jste si mohli přečíst v článku Oblast platnosti proměnné, uzávěr, proměnná začíná v „neinicializovaném“ stavu ve chvíli, kdy běh vstoupí do kódového bloku (nebo do funkce). A zůstane neinicializovaná až do příslušného příkazu let.

Jinými slovy, před let proměnná technicky existuje, ale nemůže být používána.

Uvedený kód to demonstruje.

function funkce() {
  // motor zná lokální proměnnou x od začátku této funkce,
  // ale ta je „neinicializovaná“ (nepoužitelná) až do příkazu let („mrtvá zóna“)
  // proto chyba

  console.log(x); // ReferenceError: Nelze přistupovat k 'x' před inicializací

  let x = 2;
}

Tato zóna dočasné nepoužitelnosti proměnné (od začátku kódového bloku do let) se někdy nazývá „mrtvá zóna“.

důležitost: 5

Pro pole máme vestavěnou metodu pole.filter(f), která filtruje všechny prvky pomocí funkce f. Jestliže f vrátí true, bude prvek vrácen ve výsledném poli.

Vytvořte sadu filtrů „připravených k použití“:

  • mezi(a, b) – mezi a a b nebo rovno některému z nich (inkluzívně).
  • vPoli([...]) – v zadaném poli.

Použití musí být následující:

  • pole.filter(mezi(3,6)) – vybere jen hodnoty mezi 3 a 6.
  • pole.filter(vPoli([1,2,3])) – vybere jen prvky, které se rovnají některému z prvků pole [1,2,3].

Například:

/* .. váš kód funkcí mezi a vPoli */
let pole = [1, 2, 3, 4, 5, 6, 7];

alert( pole.filter(mezi(3, 6)) ); // 3,4,5,6

alert( pole.filter(vPoli([1, 2, 10])) ); // 1,2

Otevřít pískoviště s testy.

Filtr mezi

function mezi(a, b) {
  return function(x) {
    return x >= a && x <= b;
  };
}

let pole = [1, 2, 3, 4, 5, 6, 7];
alert( pole.filter(mezi(3, 6)) ); // 3,4,5,6

Filtr vPoli

function vPoli(pole) {
  return function(x) {
    return pole.includes(x);
  };
}

let pole = [1, 2, 3, 4, 5, 6, 7];
alert( pole.filter(vPoli([1, 2, 10])) ); // 1,2

Otevřít řešení s testy na pískovišti.

důležitost: 5

Máme pole objektů, které chceme seřadit:

let uživatelé = [
  { jméno: "Jan", věk: 20, příjmení: "Janík" },
  { jméno: "Petr", věk: 18, příjmení: "Petřík" },
  { jméno: "Anna", věk: 19, příjmení: "Hadrabová" }
];

Obvyklý způsob, jak to udělat, by byl:

// podle jména (Anna, Jan, Petr)
uživatelé.sort((a, b) => a.jméno > b.jméno ? 1 : -1);

// podle věku (Petr, Anna, Jan)
uživatelé.sort((a, b) => a.věk > b.věk ? 1 : -1);

Můžeme to učinit ještě stručněji, například takto?

uživatelé.sort(podleVlastnosti('jméno'));
uživatelé.sort(podleVlastnosti('věk'));

Místo psaní funkce tedy jednoduše napíšeme podleVlastnosti(názevVlastnosti).

Napište funkci podleVlastnosti, kterou k tomu můžeme použít.

Otevřít pískoviště s testy.

function podleVlastnosti(názevVlastnosti){
  return (a, b) => a[názevVlastnosti] > b[názevVlastnosti] ? 1 : -1;
}

Otevřít řešení s testy na pískovišti.

důležitost: 5

Následující kód vytvoří pole střelci.

Každá funkce má vypsat své číslo. Ale něco je špatně…

function vytvořArmádu() {
  let střelci = [];

  let i = 0;
  while (i < 10) {
    let střelec = function() { // vytvoříme funkci střelec,
      alert( i );              // která by měla zobrazit své číslo
    };
    střelci.push(střelec); // a přidáme ji do pole
    i++;
  }

  // ...a vrátíme pole střelci
  return střelci;
}

let armáda = vytvořArmádu();

// všichni střelci zobrazí 10 místo svých čísel 0, 1, 2, 3...
armáda[0](); // 10 od střelce číslo 0
armáda[1](); // 10 od střelce číslo 1
armáda[2](); // 10 ...a tak dále.

Proč všichni střelci zobrazují stejnou hodnotu?

Opravte kód, aby fungoval tak, jak je zamýšleno.

Otevřít pískoviště s testy.

Prozkoumejme, co přesně se děje uvnitř funkce vytvořArmádu, a řešení bude zřejmé.

  1. Vytvoří se prázdné pole střelci:

    let střelci = [];
  2. V cyklu se naplní funkcemi pomocí střelci.push(function).

    Každý prvek je funkce, takže výsledné pole vypadá takto:

    střelci = [
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); }
    ];
  3. Toto pole funkce vrátí.

    Pak později volání kteréhokoli jeho prvku, např. armáda[5](), načte z pole prvek armáda[5] (což je funkce) a zavolá jej.

    Proč nyní všechny tyto funkce zobrazí stejnou hodnotu, 10?

    Je to proto, že uvnitř funkcí střelec neexistuje žádná lokální proměnná i. Když je taková funkce volána, převezme i z vnějšího lexikálního prostředí.

    Jaká pak bude hodnota proměnné i?

    Když se podíváme na zdrojový kód:

    function vytvořArmádu() {
      ...
      let i = 0;
      while (i < 10) {
        let střelec = function() { // funkce střelec
          alert( i );              // by měla zobrazit své číslo
        };
        střelci.push(střelec); // přidá funkci do pole
        i++;
      }
      ...
    }

    Vidíme, že všechny funkce střelec jsou vytvořeny v lexikálním prostředí funkce vytvořArmádu(). Když je však volána armáda[5](), funkce vytvořArmádu již ukončila svou práci a poslední hodnota i je 10 (while se zastaví na i=10).

    Výsledkem je, že všechny funkce střelec převezmou z vnějšího lexikálního prostředí stejnou hodnotu a tou bude poslední hodnota, i=10.

    Jak vidíte výše, při každé iteraci bloku while {...} bude vytvořeno nové lexikální prostředí. Abychom to opravili, můžeme zkopírovat hodnotu i do proměnné uvnitř bloku while {...} třeba takto:

    function vytvořArmádu() {
      let střelci = [];
    
      let i = 0;
      while (i < 10) {
          let j = i;
          let střelec = function() { // funkce střelec
            alert( j );              // by měla zobrazit své číslo
          };
        střelci.push(střelec);
        i++;
      }
    
      return střelci;
    }
    
    let armáda = vytvořArmádu();
    
    // Nyní kód funguje správně
    armáda[0](); // 0
    armáda[5](); // 5

    Zde let j = i deklaruje „iteračně lokální“ proměnnou j a zkopíruje do ní i. Primitivy se kopírují „hodnotou“, takže ve skutečnosti získáme nezávislou kopii i, která patří do aktuální iterace cyklu.

    Střelci budou fungovat správně, protože hodnota i nyní existuje trochu blíže. Není v lexikálním prostředí funkce vytvořArmádu(), ale v lexikálním prostředí, které odpovídá aktuální iteraci cyklu:

    Tomuto problému se lze vyhnout i tak, že na začátku použijeme for, třeba takto:

    function vytvořArmádu() {
    
      let střelci = [];
    
      for(let i = 0; i < 10; i++) {
        let střelec = function() { // funkce střelec
          alert( i );              // by měla zobrazit své číslo
        };
        střelci.push(střelec);
      }
    
      return střelci;
    }
    
    let armáda = vytvořArmádu();
    
    armáda[0](); // 0
    armáda[5](); // 5

    To je v zásadě totéž, protože for při každé své iteraci vygeneruje nové lexikální prostředí se svou vlastní proměnnou i. Takže střelec generovaný v každé iteraci odkazuje na své vlastní i přímo z této iterace.

Když jste vložili tolik námahy do přečtení tohoto řešení a konečný recept je tak jednoduchý – prostě použijeme for, můžete se divit – mělo to cenu?

Inu, kdybyste na tuto otázku dokázali snadno odpovědět, nečetli byste řešení. Snad vám tedy tato úloha pomohla trochu lépe všemu porozumět.

Kromě toho zajisté existují případy, kdy člověk dává přednost while před for, i jiné scénáře, v nichž takové problémy opravdu nastanou.

Otevřít řešení s testy na pískovišti.

Mapa tutoriálu

Komentáře

přečtěte si před komentováním…
  • Máte-li návrhy na zlepšení, vytvořte prosím issue na GitHubu nebo pull request místo komentáře.
  • Pokud v článku něčemu nerozumíte, napište prosím, čemu přesně a na kterém místě.
  • Pro vložení několika slov kódu použijte značku <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…)