Regulární funkce vrací pouze jednu, jedinou hodnotu (nebo nic).
Generátory mohou vracet („výtěžek“) více hodnot, jednu po druhé, na vyžádání. Skvěle fungují s iterovatelnými, což umožňuje snadno vytvářet datové toky.
Funkce generátoru
K vytvoření generátoru potřebujeme speciální konstrukci syntaxe:function*
, tzv. „funkce generátoru“.
Vypadá to takto:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
Funkce generátoru se chovají odlišně od běžných. Když je taková funkce volána, nespustí svůj kód. Místo toho vrací speciální objekt, nazývaný „objekt generátoru“, který řídí provádění.
Zde se podívejte:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
// "generator function" creates "generator object"
let generator = generateSequence();
alert(generator); // [object Generator]
Spouštění kódu funkce ještě nezačalo:
Hlavní metoda generátoru je next()
. Po zavolání spustí provádění až do nejbližších yield <value>
výpis (value
lze vynechat, pak je to undefined
). Poté se provádění funkce pozastaví a výsledkem je value
se vrátí do vnějšího kódu.
Výsledek next()
je vždy objekt se dvěma vlastnostmi:
value
:výnosová hodnota.done
:true
pokud kód funkce skončil, jinakfalse
.
Například zde vytvoříme generátor a získáme jeho první výslednou hodnotu:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
let generator = generateSequence();
let one = generator.next();
alert(JSON.stringify(one)); // {value: 1, done: false}
Zatím máme pouze první hodnotu a provedení funkce je na druhém řádku:
Zavolejte generator.next()
znovu. Obnoví provádění kódu a vrátí další yield
:
let two = generator.next();
alert(JSON.stringify(two)); // {value: 2, done: false}
A pokud to zavoláme potřetí, provedení dosáhne return
příkaz, který ukončí funkci:
let three = generator.next();
alert(JSON.stringify(three)); // {value: 3, done: true}
Nyní je generátor hotový. Měli bychom to vidět z done:true
a zpracujte value:3
jako konečný výsledek.
Nová volání na číslo generator.next()
už nedávají smysl. Pokud je provedeme, vrátí stejný objekt:{done: true}
.
function* f(…)
nebo function *f(…)
? Obě syntaxe jsou správné.
Obvykle je však preferována první syntaxe, jako hvězdička *
označuje, že se jedná o funkci generátoru, popisuje druh, nikoli název, takže by měla zůstat u function
klíčové slovo.
Generátory jsou iterovatelné
Jak jste již pravděpodobně uhodli při pohledu na next()
generátory jsou iterovatelné.
Jejich hodnoty můžeme přecyklovat pomocí for..of
:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
let generator = generateSequence();
for(let value of generator) {
alert(value); // 1, then 2
}
Vypadá to mnohem lépe než volání .next().value
, že?
…Ale prosím, všimněte si:výše uvedený příklad ukazuje 1
a poté 2
, a to je vše. Nezobrazuje 3
!
Je to proto, že for..of
iterace ignoruje posledních value
, když done: true
. Pokud tedy chceme, aby všechny výsledky byly zobrazeny pomocí for..of
, musíme je vrátit s yield
:
function* generateSequence() {
yield 1;
yield 2;
yield 3;
}
let generator = generateSequence();
for(let value of generator) {
alert(value); // 1, then 2, then 3
}
Jelikož jsou generátory iterovatelné, můžeme všechny související funkce, např. syntaxi šíření ...
:
function* generateSequence() {
yield 1;
yield 2;
yield 3;
}
let sequence = [0, ...generateSequence()];
alert(sequence); // 0, 1, 2, 3
Ve výše uvedeném kódu ...generateSequence()
změní objekt iterovatelného generátoru na pole položek (více o syntaxi spreadu si přečtěte v kapitole Parametry zbytku a syntaxe spreadu)
Použití generátorů pro iterovatelné
Před časem jsme v kapitole Iterables vytvořili iterovatelný range
objekt, který vrací hodnoty from..to
.
Zde si zapamatujte kód:
let range = {
from: 1,
to: 5,
// for..of range calls this method once in the very beginning
[Symbol.iterator]() {
// ...it returns the iterator object:
// onward, for..of works only with that object, asking it for next values
return {
current: this.from,
last: this.to,
// next() is called on each iteration by the for..of loop
next() {
// it should return the value as an object {done:.., value :...}
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
}
};
// iteration over range returns numbers from range.from to range.to
alert([...range]); // 1,2,3,4,5
Pro iteraci můžeme použít funkci generátoru tak, že ji poskytneme jako Symbol.iterator
.
Zde je stejný range
, ale mnohem kompaktnější:
let range = {
from: 1,
to: 5,
*[Symbol.iterator]() { // a shorthand for [Symbol.iterator]: function*()
for(let value = this.from; value <= this.to; value++) {
yield value;
}
}
};
alert( [...range] ); // 1,2,3,4,5
To funguje, protože range[Symbol.iterator]()
nyní vrací generátor a metody generátoru jsou přesně to, co for..of
očekává:
- má
.next()
metoda - které vrací hodnoty ve tvaru
{value: ..., done: true/false}
To samozřejmě není náhoda. Generátory byly přidány do jazyka JavaScript s ohledem na iterátory, aby je bylo možné snadno implementovat.
Varianta s generátorem je mnohem stručnější než původní iterovatelný kód range
a zachovává si stejnou funkcionalitu.
Ve výše uvedených příkladech jsme vygenerovali konečné sekvence, ale můžeme také vytvořit generátor, který poskytuje hodnoty navždy. Například nekonečná sekvence pseudonáhodných čísel.
To by jistě vyžadovalo break
(nebo return
) v for..of
nad takovým generátorem. Jinak by se smyčka opakovala donekonečna a viset.
Složení generátoru
Složení generátoru je speciální funkcí generátorů, která umožňuje transparentně „vložit“ generátory do sebe.
Máme například funkci, která generuje posloupnost čísel:
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
Nyní bychom jej rádi znovu použili ke generování složitější sekvence:
- nejprve číslice
0..9
(s kódy znaků 48…57), - následují velká písmena abecedy
A..Z
(kódy znaků 65…90) - následují malá písmena abecedy
a..z
(kódy znaků 97…122)
Tuto sekvenci můžeme použít např. vytvářet hesla tak, že z nich vyberete znaky (můžete přidat i znaky syntaxe), ale nejprve je vygenerujeme.
Chcete-li v běžné funkci zkombinovat výsledky z více dalších funkcí, zavoláme je, uložíme výsledky a na konci je spojíme.
Pro generátory existuje speciální yield*
syntaxe pro „vložení“ (složení) jednoho generátoru do druhého.
Složený generátor:
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
function* generatePasswordCodes() {
// 0..9
yield* generateSequence(48, 57);
// A..Z
yield* generateSequence(65, 90);
// a..z
yield* generateSequence(97, 122);
}
let str = '';
for(let code of generatePasswordCodes()) {
str += String.fromCharCode(code);
}
alert(str); // 0..9A..Za..z
yield*
direktiva delegates provedení na jiný generátor. Tento výraz znamená yield* gen
iteruje přes generátor gen
a transparentně převádí své výnosy ven. Jako by hodnoty poskytoval vnější generátor.
Výsledek je stejný, jako kdybychom vložili kód z vnořených generátorů:
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
function* generateAlphaNum() {
// yield* generateSequence(48, 57);
for (let i = 48; i <= 57; i++) yield i;
// yield* generateSequence(65, 90);
for (let i = 65; i <= 90; i++) yield i;
// yield* generateSequence(97, 122);
for (let i = 97; i <= 122; i++) yield i;
}
let str = '';
for(let code of generateAlphaNum()) {
str += String.fromCharCode(code);
}
alert(str); // 0..9A..Za..z
Složení generátoru je přirozený způsob, jak vložit tok jednoho generátoru do druhého. K ukládání mezivýsledků nevyužívá extra paměť.
„výnos“ je obousměrná ulice
Až do tohoto okamžiku byly generátory podobné iterovatelným objektům se speciální syntaxí pro generování hodnot. Ale ve skutečnosti jsou mnohem výkonnější a flexibilnější.
To proto, že yield
je obousměrná ulice:nejen že vrací výsledek ven, ale také může předat hodnotu uvnitř generátoru.
K tomu bychom měli zavolat generator.next(arg)
, s argumentem. Tento argument se stane výsledkem yield
.
Podívejme se na příklad:
function* gen() {
// Pass a question to the outer code and wait for an answer
let result = yield "2 + 2 = ?"; // (*)
alert(result);
}
let generator = gen();
let question = generator.next().value; // <-- yield returns the value
generator.next(4); // --> pass the result into the generator
- První volání
generator.next()
by měl být vždy proveden bez argumentu (argument je ignorován, pokud je předán). Spustí provádění a vrátí výsledek prvníhoyield "2+2=?"
. V tomto okamžiku generátor pozastaví provádění, zatímco zůstane na řádku(*)
. - Potom, jak je znázorněno na obrázku výše, výsledek
yield
dostane doquestion
proměnná ve volacím kódu. - Na
generator.next(4)
, generátor se obnoví a4
dostane se jako výsledek:let result = 4
.
Upozorňujeme, že vnější kód nemusí okamžitě volat next(4)
. Může to chvíli trvat. To není problém:generátor počká.
Například:
// resume the generator after some time
setTimeout(() => generator.next(4), 1000);
Jak vidíme, na rozdíl od běžných funkcí si generátor a volající kód mohou vyměňovat výsledky předáním hodnot v next/yield
.
Aby to bylo jasnější, zde je další příklad s více hovory:
function* gen() {
let ask1 = yield "2 + 2 = ?";
alert(ask1); // 4
let ask2 = yield "3 * 3 = ?"
alert(ask2); // 9
}
let generator = gen();
alert( generator.next().value ); // "2 + 2 = ?"
alert( generator.next(4).value ); // "3 * 3 = ?"
alert( generator.next(9).done ); // true
Obrázek provedení:
- První
.next()
spustí provádění... Dosáhne prvníhoyield
. - Výsledek se vrátí do vnějšího kódu.
- Druhý
.next(4)
projde4
zpět do generátoru jako výsledek prvníhoyield
a obnoví provádění. - ...Dosáhne druhého
yield
, který se stane výsledkem volání generátoru. - Třetí
next(9)
projde9
do generátoru jako výsledek druhéhoyield
a obnoví provádění, které dosáhne konce funkce, takžedone: true
.
Je to jako hra „ping-pong“. Každý next(value)
(kromě prvního) předá do generátoru hodnotu, která se stane výsledkem aktuálního yield
a poté získá zpět výsledek dalšího yield
.
generator.throw
Jak jsme viděli ve výše uvedených příkladech, vnější kód může předat hodnotu do generátoru jako výsledek yield
.
…Ale také tam může iniciovat (vyhodit) chybu. To je přirozené, protože chyba je druh výsledku.
Předání chyby do yield
, měli bychom zavolat generator.throw(err)
. V takovém případě err
je vhozen do řádku s tímto yield
.
Například zde výnos "2 + 2 = ?"
vede k chybě:
function* gen() {
try {
let result = yield "2 + 2 = ?"; // (1)
alert("The execution does not reach here, because the exception is thrown above");
} catch(e) {
alert(e); // shows the error
}
}
let generator = gen();
let question = generator.next().value;
generator.throw(new Error("The answer is not found in my database")); // (2)
Chyba, vržená do generátoru na řádku (2)
vede k výjimce v řádku (1)
s yield
. Ve výše uvedeném příkladu try..catch
chytí to a ukáže to.
Pokud jej nezachytíme, pak stejně jako každá výjimka „vypadne“ z generátoru do volacího kódu.
Aktuální řádek volajícího kódu je řádek s generator.throw
, označený jako (2)
. Takže to můžeme chytit tady, takhle:
function* generate() {
let result = yield "2 + 2 = ?"; // Error in this line
}
let generator = generate();
let question = generator.next().value;
try {
generator.throw(new Error("The answer is not found in my database"));
} catch(e) {
alert(e); // shows the error
}
Pokud tam chybu nezachytíme, pak, jako obvykle, propadne vnějšímu volajícímu kódu (pokud existuje) a pokud není zachycen, skript zabije.
generator.return
generator.return(value)
dokončí provádění generátoru a vrátí dané value
.
function* gen() {
yield 1;
yield 2;
yield 3;
}
const g = gen();
g.next(); // { value: 1, done: false }
g.return('foo'); // { value: "foo", done: true }
g.next(); // { value: undefined, done: true }
Pokud znovu použijeme generator.return()
v dokončeném generátoru vrátí tuto hodnotu znovu (MDN).
Často to nepoužíváme, protože většinou chceme získat všechny návratové hodnoty, ale může být užitečné, když chceme zastavit generátor v konkrétním stavu.
Shrnutí
- Generátory jsou vytvářeny funkcemi generátoru
function* f(…) {…}
. - Uvnitř generátorů (pouze) existuje
yield
operátor. - Vnější kód a generátor si mohou vyměňovat výsledky prostřednictvím
next/yield
hovory.
V moderním JavaScriptu se generátory používají jen zřídka. Někdy se ale hodí, protože schopnost funkce vyměňovat si data s volajícím kódem během provádění je zcela unikátní. A jistě jsou skvělé pro vytváření iterovatelných objektů.
V další kapitole se také naučíme asynchronní generátory, které se používají ke čtení proudů asynchronně generovaných dat (např. stránkovaná načtení přes síť) v for await ... of
smyčky.
Při programování webu často pracujeme se streamovanými daty, takže to je další velmi důležitý případ použití.