Autor si vybral nadaci Open Internet/Free Speech Fund, které přispěl v rámci programu Write for DOnations.
Úvod
V ECMAScriptu 2015 byly do jazyka JavaScript zavedeny generátory. Generátor je proces, který lze pozastavit a obnovit a který může přinést více hodnot. Generátor v jazyce JavaScript se skládá z generátorové funkce, která vrací iterovatelný objekt Generator
.
Generátory mohou udržovat stav, což poskytuje efektivní způsob vytváření iterátorů, a jsou schopny pracovat s nekonečnými datovými toky, což lze využít k implementaci nekonečného posouvání na frontend webové aplikace, k práci s daty zvukových vln a dalším. Navíc při použití s Promises mohou generátory napodobovat funkci async/await
, což nám umožňuje pracovat s asynchronním kódem přímočařejším a čitelnějším způsobem. Ačkoli je async/await
rozšířenějším způsobem, jak řešit běžné, jednoduché případy asynchronního použití, jako je načítání dat z API, generátory mají pokročilejší funkce, kvůli kterým se vyplatí naučit se je používat.
V tomto článku se budeme zabývat tím, jak vytvářet funkce generátoru, jak iterovat nad objekty Generator
, rozdílem mezi yield
a return
uvnitř generátoru a dalšími aspekty práce s generátory.
Generátorové funkce
Generátorová funkce je funkce, která vrací objekt Generator
a je definována klíčovým slovem function
následovaným hvězdičkou (*
), jak ukazuje následující obrázek:
// Generator function declarationfunction* generatorFunction() {}
Občas se hvězdička objeví vedle názvu funkce, na rozdíl od klíčového slova funkce, například function *generatorFunction()
. Funguje to stejně, ale function*
je rozšířenější syntaxe.
Generátorové funkce lze také definovat ve výrazu, podobně jako regulární funkce:
// Generator function expressionconst generatorFunction = function*() {}
Generátory mohou být dokonce metodami objektu nebo třídy:
V příkladech v tomto článku bude použita syntaxe deklarace generátorové funkce.
Poznámka: Na rozdíl od běžných funkcí nelze generátory konstruovat pomocí klíčového slova new
, ani je nelze použít ve spojení s funkcemi šipek.
Teď, když víte, jak deklarovat generátorové funkce, podívejme se na iterovatelné Generator
objekty, které vracejí.
Generátorové objekty
Tradičně funkce v JavaScriptu běží až do konce a volání funkce vrátí hodnotu, jakmile dojde ke klíčovému slovu return
. Pokud je klíčové slovo return
vynecháno, funkce implicitně vrátí undefined
.
V následujícím kódu například deklarujeme funkci sum()
, která vrací hodnotu, jež je součtem dvou celočíselných argumentů:
// A regular function that sums two valuesfunction sum(a, b) { return a + b}
Volání funkce vrací hodnotu, která je součtem argumentů:
const value = sum(5, 6) // 11
Funkce generátoru však nevrací hodnotu okamžitě a místo toho vrací iterovatelný objekt Generator
. V následujícím příkladu deklarujeme funkci a dáme jí jedinou návratovou hodnotu jako standardní funkci:
// Declare a generator function with a single return valuefunction* generatorFunction() { return 'Hello, Generator!'}
Když zavoláme generátorovou funkci, vrátí objekt Generator
, který můžeme přiřadit proměnné:
// Assign the Generator object to generatorconst generator = generatorFunction()
Pokud by se jednalo o běžnou funkci, očekávali bychom, že generator
nám vrátí řetězec vrácený ve funkci. Ve skutečnosti však dostaneme objekt ve stavu suspended
. Volání funkce generator
tedy poskytne výstup podobný následujícímu:
Objekt Generator
vrácený funkcí je iterátor. Iterátor je objekt, který má k dispozici metodu next()
, která slouží k iteraci posloupnosti hodnot. Metoda next()
vrací objekt s vlastnostmi value
a done
. value
představuje vrácenou hodnotu a done
udává, zda iterátor proběhl všemi hodnotami, nebo ne.
Víme-li toto, zavoláme next()
na náš generator
a zjistíme aktuální hodnotu a stav iterátoru:
// Call the next method on the Generator objectgenerator.next()
Tím získáme následující výstup:
Output{value: "Hello, Generator!", done: true}
Vrácená hodnota z volání next()
je Hello, Generator!
a stav done
je true
, protože tato hodnota pochází z return
, který iterátor uzavřel. Protože je iterátor ukončen, změní se stav funkce generátoru z suspended
na closed
. Opětovným voláním funkce generator
získáme následující:
OutputgeneratorFunction {<closed>}
V tuto chvíli jsme si pouze ukázali, jak může být generátorová funkce složitějším způsobem, jak získat hodnotu return
funkce. Generátorové funkce však mají také jedinečné vlastnosti, které je odlišují od běžných funkcí. V příští části se seznámíme s operátorem yield
a uvidíme, jak může generátor pozastavit a obnovit provádění.
operátory yield
Generátory zavádějí do jazyka JavaScript nové klíčové slovo: yield
. yield
dokáže pozastavit funkci generátoru a vrátit hodnotu, která následuje po yield
, čímž poskytuje lehký způsob iterace hodnot.
V tomto příkladu pozastavíme funkci generátoru třikrát s různými hodnotami a na konci vrátíme hodnotu. Pak přiřadíme náš objekt Generator
do proměnné generator
.
Nyní, když zavoláme next()
na funkci generátoru, pozastaví se pokaždé, když narazí na yield
. Po každém yield
bude done
nastavena na false
, což znamená, že generátor ještě neskončil. Jakmile narazí na return
nebo jakmile se ve funkci již nevyskytnou žádné další yield
, done
se překlopí na true
a generátor bude ukončen.
Použijeme-li metodu next()
čtyřikrát za sebou:
// Call next four timesgenerator.next()generator.next()generator.next()generator.next()
Ty dají následující čtyři řádky výstupu v pořadí:
Output{value: "Neo", done: false}{value: "Morpheus", done: false}{value: "Trinity", done: false}{value: "The Oracle", done: true}
Všimněte si, že generátor nevyžaduje return
; pokud je vynechán, poslední iterace vrátí {value: undefined, done: true}
, stejně jako každé další volání next()
po dokončení generátoru.
Iterace nad generátorem
Pomocí metody next()
jsme ručně iterovali objekt Generator
a získali všechny vlastnosti value
a done
celého objektu. Stejně jako Array
, Map
a Set
se však i Generator
řídí iteračním protokolem a lze jej iterovat pomocí for...of
:
// Iterate over Generator objectfor (const value of generator) { console.log(value)}
To vrátí následující:
OutputNeoMorpheusTrinity
Operátor rozprostření lze také použít k přiřazení hodnot Generator
do pole.
// Create an array from the values of a Generator objectconst values = console.log(values)
Tím získáme následující pole:
Output(3)
Operátor spread i for...of
nezapočítají do hodnot return
(v tomto případě by to bylo 'The Oracle'
).
Poznámka: Zatímco obě tyto metody jsou účinné pro práci s konečnými generátory, pokud generátor pracuje s nekonečným datovým tokem, nebude možné použít přímo spread nebo for...of
, aniž by se vytvořila nekonečná smyčka.
Zavření generátoru
Jak jsme viděli, generátor může mít svou vlastnost done
nastavenou na true
a svůj stav na closed
iterací přes všechny své hodnoty. Existují dva další způsoby, jak generátor okamžitě zrušit: metodou return()
a metodou throw()
.
Pomocí return()
lze generátor ukončit v libovolném okamžiku, stejně jako kdyby byl v těle funkce příkaz return
. Do return()
můžete předat argument nebo jej nechat prázdný pro nedefinovanou hodnotu.
Pro demonstraci return()
vytvoříme generátor s několika hodnotami yield
, ale bez return
v definici funkce:
function* generatorFunction() { yield 'Neo' yield 'Morpheus' yield 'Trinity'}const generator = generatorFunction()
První next()
nám dá 'Neo'
, přičemž done
nastavíme na false
. Pokud hned poté vyvoláme metodu return()
na objektu Generator
, dostaneme nyní předanou hodnotu a done
nastavenou na true
. Jakékoli další volání next()
poskytne výchozí dokončenou odpověď generátoru s nedefinovanou hodnotou.
Pro demonstraci tohoto postupu spusťte následující tři metody na generator
:
generator.next()generator.return('There is no spoon!')generator.next()
Tím získáme následující tři výsledky:
Output{value: "Neo", done: false}{value: "There is no spoon!", done: true}{value: undefined, done: true}
Metoda return()
donutila objekt Generator
dokončit a ignorovat všechna další klíčová slova yield
. To je užitečné zejména při asynchronním programování, kdy potřebujete zajistit, aby bylo možné funkce zrušit, například přerušit webový požadavek, když chce uživatel provést jinou akci, protože není možné přímo zrušit slib.
Pokud má tělo funkce generátoru způsob, jak zachytit a řešit chyby, můžete použít metodu throw()
, abyste do generátoru vyhodili chybu. Tím se generátor spustí, vyhodí se do něj chyba a generátor se ukončí.
Abychom si to demonstrovali, vložíme do těla funkce generátoru metodu try...catch
a v případě nalezení chyby ji zaznamenáme:
Nyní spustíme metodu next()
a po ní metodu throw()
:
generator.next()generator.throw(new Error('Agent Smith!'))
Tím získáme následující výstup:
Output{value: "Neo", done: false}Error: Agent Smith!{value: undefined, done: true}
Pomocí throw()
jsme do generátoru vložili chybu, která byla zachycena try...catch
a zaznamenána do konzoly.
Metody a stavy objektu generátoru
V následující tabulce je uveden seznam metod, které lze použít na objektech Generator
:
Metoda | Popis |
---|---|
next() |
Vrátí další hodnotu v generátoru |
return() |
Vrátí hodnotu v generátoru a ukončí generátor |
throw() |
Vyhodí chybu a ukončí generátor |
V další tabulce jsou uvedeny možné stavy objektu Generator
:
Stav | Popis |
---|---|
suspended |
Generátor zastavil provádění, ale neukončil se |
closed |
Generátor se ukončil buď tím, že došlo k chybě, vrácením, nebo iterací všech hodnot |
yield Delegation
Kromě běžného operátoru yield
mohou generátory používat také výraz yield*
pro delegování dalších hodnot na jiný generátor. Když na yield*
narazíte uvnitř generátoru, přejde dovnitř delegovaného generátoru a začne iterovat přes všechny yield
, dokud se tento generátor neuzavře. Toho lze využít k oddělení různých funkcí generátoru, abyste sémanticky uspořádali kód, a zároveň aby všechny jejich yield
byly iterovatelné ve správném pořadí.
Pro demonstraci můžeme vytvořit dvě generátorové funkce, z nichž jedna bude yield*
operovat s druhou:
Dále budeme iterovat přes generátorovou funkci begin()
:
// Iterate through the outer generatorconst generator = begin()for (const value of generator) { console.log(value)}
Tím získáme následující hodnoty v pořadí, v jakém jsou generovány:
Output1234
Vnější generátor dal hodnoty 1
a 2
, pak se delegoval na druhý generátor s yield*
, který vrátil 3
a 4
.
yield*
může také delegovat na libovolný iterovatelný objekt, například pole nebo mapu. Delegování výnosů může být užitečné při organizaci kódu, protože každá funkce v generátoru, která by chtěla použít yield
, by musela být také generátorem.
Konečné datové toky
Jedním z užitečných aspektů generátorů je možnost pracovat s nekonečnými datovými toky a kolekcemi. To lze demonstrovat vytvořením nekonečné smyčky uvnitř funkce generátoru, která zvětšuje číslo o jedničku.
V následujícím bloku kódu definujeme tuto generátorovou funkci a poté generátor spustíme:
Nyní iterujeme přes hodnoty pomocí next()
:
// Iterate through the valuescounter.next()counter.next()counter.next()counter.next()
Tím získáme následující výstup:
Output{value: 0, done: false}{value: 1, done: false}{value: 2, done: false}{value: 3, done: false}
Funkce vrací postupné hodnoty v nekonečné smyčce, přičemž vlastnost done
zůstává false
, což zajistí, že neskončí.
S generátory se nemusíte obávat vytvoření nekonečné smyčky, protože můžete libovolně zastavit a obnovit provádění. Stále však musíte být obezřetní při způsobu vyvolání generátoru. Pokud použijete spread nebo for...of
na nekonečný proud dat, budete stále iterovat přes nekonečnou smyčku najednou, což způsobí pád prostředí.
Pro složitější příklad nekonečného proudu dat můžeme vytvořit funkci generátoru Fibonacci. Fibonacciho posloupnost, která nepřetržitě sčítá dvě předchozí hodnoty dohromady, lze zapsat pomocí nekonečné smyčky v generátoru takto:
Pro vyzkoušení můžeme smyčkou projít konečné číslo a vypsat Fibonacciho posloupnost na konzolu.
// Print the first 10 values of fibonacciconst fib = fibonacci()for (let i = 0; i < 10; i++) { console.log(fib.next().value)}
Tím získáme následující:
Output0112358132134
Schopnost pracovat s nekonečnými soubory dat je jednou z částí toho, co dělá generátory tak mocnými. To může být užitečné pro příklady, jako je implementace nekonečného rolování na frontend webové aplikace.
Předávání hodnot v generátorech
V celém tomto článku jsme používali generátory jako iterátory a v každé iteraci jsme dávali hodnoty. Kromě toho, že generátory produkují hodnoty, mohou také hodnoty spotřebovávat next()
. V tomto případě bude yield
obsahovat hodnotu.
Je důležité si uvědomit, že první zavolaný next()
nepředá hodnotu, ale pouze spustí generátor. Abychom to demonstrovali, můžeme zaznamenat hodnotu yield
a několikrát zavolat next()
s nějakými hodnotami.
Tím získáme následující výstup:
Output100200{value: "The end", done: true}
Je také možné generátor nasadit počáteční hodnotou. V následujícím příkladu vytvoříme cyklus for
a každou hodnotu předáme do metody next()
, ale počáteční funkci předáme také argument:
Získáme hodnotu z next()
a další iteraci předáme novou hodnotu, což je předchozí hodnota krát deset. Tím získáme následující:
Output010203040
Jiný způsob, jak se vypořádat se spuštěním generátoru, je zabalit generátor do funkce, která vždy jednou zavolá next()
, než provede cokoli dalšího.
async/await s generátory
Asynchronní funkce je typ funkce dostupný v JavaScriptu ES6+, který usnadňuje práci s asynchronními daty tím, že se jeví jako synchronní. Generátory mají rozsáhlejší škálu možností než asynchronní funkce, ale jsou schopny replikovat podobné chování. Implementace asynchronního programování tímto způsobem může zvýšit flexibilitu vašeho kódu.
V této části si ukážeme příklad reprodukce async
/await
pomocí generátorů.
Sestavíme asynchronní funkci, která pomocí rozhraní Fetch API získá data z rozhraní JSONPlaceholder API (které poskytuje ukázková data JSON pro účely testování) a zaznamená odpověď do konzoly.
Začněte tím, že definujete asynchronní funkci s názvem getUsers
, která načte data z API a vrátí pole objektů, pak zavolejte getUsers
:
Tím získáte data JSON podobná následujícím:
Pomocí generátorů můžeme vytvořit něco téměř identického, co nepoužívá klíčová slova async
/await
. Místo toho bude používat námi vytvořenou novou funkci a hodnoty yield
místo slibů await
.
V následujícím bloku kódu definujeme funkci getUsers
, která používá naši novou funkci asyncAlt
(kterou napíšeme později), aby napodobila async
/await
.
Jak vidíme, vypadá to téměř identicky jako implementace async
/await
, až na to, že se předává funkce generátoru, která dává hodnoty.
Nyní můžeme vytvořit funkci asyncAlt
, která se podobá asynchronní funkci. Funkce asyncAlt
má jako parametr generátorovou funkci, což je naše funkce, která dává sliby, které vrací funkce fetch
. asyncAlt
sama vrací funkci a řeší každý slib, který najde, až do posledního:
Tím získáme stejný výstup jako u verze async
/await
:
Všimněte si, že tato implementace slouží k demonstraci toho, jak lze generátory použít místo async
/await
, a není to návrh připravený k výrobě. Nemá nastaveno zpracování chyb ani možnost předávat parametry do generovaných hodnot. Ačkoli tato metoda může vašemu kódu dodat flexibilitu, často bude lepší volbou async/await
, protože abstrahuje od implementačních detailů a umožňuje vám soustředit se na psaní produktivního kódu.
Závěr
Generátory jsou procesy, které mohou zastavit a obnovit provádění. Jsou mocnou a všestrannou vlastností jazyka JavaScript, i když se běžně nepoužívají. V tomto kurzu jsme se seznámili s generátorovými funkcemi a objekty generátorů, metodami dostupnými generátorům, operátory yield
a yield*
a generátory používanými s konečnými a nekonečnými množinami dat. Prozkoumali jsme také jeden ze způsobů, jak implementovat asynchronní kód bez vnořených zpětných volání nebo dlouhých řetězců slibů.
Pokud se chcete dozvědět více o syntaxi jazyka JavaScript, podívejte se na naše výukové programy Rozumíme tomuto, Bind, Call a Apply v jazyce JavaScript a Rozumíme objektům Map a Set v jazyce JavaScript.