BÄžné funkce vracejà jen jednu jedinou hodnotu (nebo žádnou).
Generátory mohou na požádánà vrátit (vydat, anglicky âyieldâ) vÃce hodnot, jednu po druhé. VýbornÄ spolupracujà s iterovatelnými objekty a umožÅujà nám snadno vytváÅet datové proudy.
Generátorové funkce
K vytvoÅenà generátoru potÅebujeme speciálnà syntaktický konstrukt: function*, tzv. âgenerátorovou funkciâ.
Vypadá takto:
function* generujPosloupnost() {
yield 1;
yield 2;
return 3;
}
Generátorové funkce se chovajà jinak než obyÄejné. Když je taková funkce volána, nespustà se jejà kód. MÃsto toho vrátà speciálnà objekt, zvaný âgenerátorový objektâ, aby jejà bÄh ÅÃdil.
PodÃvejme se:
function* generujPosloupnost() {
yield 1;
yield 2;
return 3;
}
// âgenerátorová funkceâ vytvoÅà âgenerátorový objektâ
let generátor = generujPosloupnost();
alert(generátor); // [object Generator]
BÄh kódu funkce jeÅ¡tÄ nezaÄal:
Hlavnà metoda generátoru je next(). Když je volána, spustà bÄh funkce až do nejbližšÃho pÅÃkazu yield <hodnota> (hodnota nemusà být uvedena, pak je undefined). Pak se bÄh funkce pÅerušà a vydaná hodnota je vrácena vnÄjÅ¡Ãmu kódu.
Výsledkem funkce next() je vždy objekt se dvÄma vlastnostmi:
value: vydaná hodnota.done:true, jestliže kód funkce skonÄil, jinakfalse.
NapÅÃklad zde vytvoÅÃme generátor a zÃskáme jeho prvnà vydanou hodnotu:
function* generujPosloupnost() {
yield 1;
yield 2;
return 3;
}
let generátor = generujPosloupnost();
let jedna = generátor.next();
alert(JSON.stringify(jedna)); // {value: 1, done: false}
ProzatÃm jsme zÃskali jenom prvnà hodnotu a bÄh funkce je na druhém Åádku:
Volejme znovu generátor.next(). Metoda obnovà bÄh kódu a vrátà dalšà yield:
let dvÄ = generátor.next();
alert(JSON.stringify(dvÄ)); // {value: 2, done: false}
A když ji zavoláme potÅetÃ, bÄh dosáhne pÅÃkazu return, který funkci ukonÄÃ:
let tÅi = generátor.next();
alert(JSON.stringify(tÅi)); // {value: 3, done: true}
Nynà generátor skonÄil. MÄli bychom to poznat z done:true a jako poslednà výsledek zpracovat value:3.
Dalšà volánà generátor.next() už nebudou mÃt smysl. Pokud je uÄinÃme, vrátà stejný objekt: {done: true}.
function* f(â¦) nebo function *f(â¦)?ObÄ syntaxe jsou správné.
Obvykle se vÅ¡ak dává pÅednost prvnà syntaxi, jelikož hvÄzdiÄka * oznaÄuje, že jde o generátorovou funkci. Popisuje druh funkce a ne jejà název, takže by mÄla být pÅipojena ke klÃÄovému slovu function.
Generátory jsou iterovatelné
Jak jste již pravdÄpodobnÄ usoudili z pohledu na metodu next(), generátory jsou iterovatelné.
Můžeme cyklovat nad jejich hodnotami pomocà for..of:
function* generujPosloupnost() {
yield 1;
yield 2;
return 3;
}
let generátor = generujPosloupnost();
for(let hodnota of generátor) {
alert(hodnota); // 1, pak 2
}
Vypadá to mnohem lépe než volánà .next().value, že?
â¦Ale prosÃme vÅ¡imnÄte si: uvedený pÅÃklad zobrazà 1, pak 2 a to je vÅ¡e. Nezobrazà 3!
Je to proto, že iterace for..of ignoruje poslednà hodnotu value, když je done: true. Jestliže tedy chceme, aby for..of zobrazilo vÅ¡echny výsledky, musÃme je vrátit pomocà yield:
function* generujPosloupnost() {
yield 1;
yield 2;
yield 3;
}
let generátor = generujPosloupnost();
for(let hodnota of generátor) {
alert(hodnota); // 1, pak 2, pak 3
}
Protože generátory jsou iterovatelné, můžeme použÃvat veÅ¡kerou funkcionalitu vztahujÃcà se k iterovatelným objektům, napÅ. roztaženou syntaxi ...:
function* generujPosloupnost() {
yield 1;
yield 2;
yield 3;
}
let posloupnost = [0, ...generujPosloupnost()];
alert(posloupnost); // 0, 1, 2, 3
V uvedeném kódu ...generujPosloupnost() pÅetvoÅà iterovatelný generátorový objekt na pole prvků (vÃce o roztažené syntaxi si pÅeÄtÄte v kapitole Zbytkové parametry a roztažená syntaxe).
PoužÃvánà generátorů pro iterovatelné objekty
PÅed nÄjakou dobou, v kapitole Iterovatelné objekty, jsme vytvoÅili objekt rozsah, který vracel hodnoty zaÄátek..konec.
Zde si tento kód pÅipomeÅme:
let rozsah = {
zaÄátek: 1,
konec: 5,
// for..of na objektu rozsah volá tuto metodu jednou na samém zaÄátku
[Symbol.iterator]() {
// ...vracà objekt iterátoru:
// od této chvÃle for..of pracuje jen s tÃmto objektem a ptá se ho na dalšà hodnoty
return {
aktuálnÃ: this.zaÄátek,
poslednÃ: this.konec,
// next() je volána cyklem for..of pÅi každé iteraci
next() {
// mÄla by vrátit hodnotu jako objekt {done:.., value :...}
if (this.aktuálnà <= this.poslednÃ) {
return { done: false, value: this.aktuálnÃ++ };
} else {
return { done: true };
}
}
};
}
};
// iterace nad rozsahem vracà ÄÃsla od rozsah.zaÄátek do rozsah.konec
alert([...rozsah]); // 1,2,3,4,5
Pro iteraci můžeme použÃt generátorovou funkci, když ji poskytneme jako Symbol.iterator.
Zde je stejný rozsah, ale mnohem kompaktnÄjÅ¡Ã:
let rozsah = {
zaÄátek: 1,
konec: 5,
*[Symbol.iterator]() { // zkratka pro [Symbol.iterator]: function*()
for(let hodnota = this.zaÄátek; hodnota <= this.konec; hodnota++) {
yield hodnota;
}
}
};
alert( [...rozsah] ); // 1,2,3,4,5
Funguje to, protože rozsah[Symbol.iterator]() nynà vracà generátor a metody generátoru jsou pÅesnÄ to, co for..of oÄekává:
- obsahuje metodu
.next() - která vracà hodnoty ve tvaru
{value: ..., done: true/false}
To samozÅejmÄ nenà náhoda. Generátory byly do jazyka JavaScript pÅidány s ohledem na iterátory, aby je bylo možné lehce implementovat.
Varianta s generátorem je mnohem struÄnÄjšà než původnà iterovatelný kód objektu rozsah a udržuje si stejnou funkcionalitu.
V uvedených pÅÃkladech jsme generovali koneÄné posloupnosti, ale můžeme vytvoÅit i generátor, který vydává hodnoty neustále. NapÅÃklad nekoneÄnou posloupnost pseudonáhodných ÄÃsel.
To by samozÅejmÄ vyžadovalo break (nebo return) v cyklu for..of nad takovým generátorem. Jinak by se cyklus opakoval donekoneÄna a skript by zůstal viset.
Skládánà generátorů
Skládánà generátorů je speciálnà vlastnost generátorů, která je umožÅuje průhlednÄ âzanoÅitâ do sebe navzájem.
NapÅÃklad máme funkci, která generuje posloupnost ÄÃsel:
function* generujPosloupnost(zaÄátek, konec) {
for (let i = zaÄátek; i <= konec; i++) yield i;
}
Nynà bychom ji rádi znovu použili, aby generovala složitÄjšà posloupnost:
- nejprve ÄÃslice
0..9(s kódy znaků 48 až 57), - po nich pÃsmena velké abecedy
A..Z(s kódy znaků 65 až 90), - po nich pÃsmena malé abecedy
a..z(s kódy znaků 97 až 122).
Tuto posloupnost můžeme použÃt napÅÃklad k vytváÅenà hesel tÃm, že z nà budeme vybÃrat znaky (můžeme pÅidat i syntaktické znaky), ale napÅed ji vygenerujme.
Když chceme v obyÄejné funkci zkombinovat výsledky z nÄkolika jiných funkcÃ, zavoláme je, uložÃme si jejich výsledky a nakonec je spojÃme.
Pro generátory existuje speciálnà syntaxe yield* k âvnoÅenÃâ (složenÃ) jednoho generátoru do jiného.
Složený generátor:
function* generujPosloupnost(zaÄátek, konec) {
for (let i = zaÄátek; i <= konec; i++) yield i;
}
function* generujKódyHesel() {
// 0..9
yield* generujPosloupnost(48, 57);
// A..Z
yield* generujPosloupnost(65, 90);
// a..z
yield* generujPosloupnost(97, 122);
}
let ÅetÄzec = '';
for(let kód of generujKódyHesel()) {
ÅetÄzec += String.fromCharCode(kód);
}
alert(ÅetÄzec); // 0..9A..Za..z
Direktiva yield* deleguje výkon na jiný generátor. Tento pojem znamená, že yield* gen iteruje nad generátorem gen a průhlednÄ pÅedává jeho výstupy ven, jako by byly vydány vnÄjÅ¡Ãm generátorem.
Výsledek je stejný, jako kdybychom vložili kód z vnoÅených generátorů:
function* generujPosloupnost(zaÄátek, konec) {
for (let i = zaÄátek; i <= konec; i++) yield i;
}
function* generujAlfanumerickou() {
// yield* generujPosloupnost(48, 57);
for (let i = 48; i <= 57; i++) yield i;
// yield* generujPosloupnost(65, 90);
for (let i = 65; i <= 90; i++) yield i;
// yield* generujPosloupnost(97, 122);
for (let i = 97; i <= 122; i++) yield i;
}
let ÅetÄzec = '';
for(let kód of generujAlfanumerickou()) {
ÅetÄzec += String.fromCharCode(kód);
}
alert(ÅetÄzec); // 0..9A..Za..z
Skládánà generátorů je pÅirozený způsob, jak vložit bÄh jednoho generátoru do druhého. NepoužÃvá dalšà pamÄÅ¥, do nÞ by se ukládaly průbÄžné výsledky.
âyieldâ je obousmÄrná ulice
Až dosud se generátory podobaly iterovatelným objektům se speciálnà syntaxà pro generovánà hodnot. Ve skuteÄnosti vÅ¡ak jsou mnohem silnÄjšà a flexibilnÄjÅ¡Ã.
Je to proto, že yield je obousmÄrná ulice: nejenom vracà výsledek ven, ale také umà pÅedat hodnotu dovnitÅ generátoru.
Abychom tak uÄinili, mÄli bychom volat generátor.next(arg) s argumentem. Tento argument se stane výsledkem yield.
PodÃvejme se na pÅÃklad:
function* gen() {
// PÅedá otázku vnÄjÅ¡Ãmu kódu a poÄká na odpovÄÄ
let výsledek = yield "2 + 2 = ?"; // (*)
alert(výsledek);
}
let generátor = gen();
let otázka = generátor.next().value; // <-- yield vrátà hodnotu
generátor.next(4); // --> pÅedá výsledek do generátoru
- Prvnà volánÃ
generátor.next()by mÄlo být vždy uÄinÄno bez argumentu (je-li argument pÅedán, je ignorován). Zahájà bÄh a vrátà výsledek prvnÃhoyield "2+2=?". V tuto chvÃli generátor pozastavà svůj bÄh a zůstane na Åádku(*). - Pak, jak je znázornÄno na obrázku, se výsledek
yieldstane hodnotou promÄnnéotázkave volajÃcÃm kódu. - Na
generátor.next(4)se generátor obnovà a jako výsledek zÃská4:let výsledek = 4.
ProsÃme vÅ¡imnÄte si, že vnÄjšà kód nemusà volat next(4) okamžitÄ. Může to nÄjakou dobu trvat. To nenà problém: generátor poÄká.
NapÅÃklad:
// obnovà generátor za nÄjakou dobu
setTimeout(() => generátor.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ávánÃm hodnot v pÅÃkazech next/yield.
Aby to bylo jasnÄjÅ¡Ã, následuje dalšà pÅÃklad s vÃce volánÃmi:
function* gen() {
let dotaz1 = yield "2 + 2 = ?";
alert(dotaz1); // 4
let dotaz2 = yield "3 * 3 = ?"
alert(dotaz2); // 9
}
let generátor = gen();
alert( generátor.next().value ); // "2 + 2 = ?"
alert( generátor.next(4).value ); // "3 * 3 = ?"
alert( generátor.next(9).done ); // true
BÄh kódu vidÃme na obrázku:
- PrvnÃ
.next()zahájà bÄh⦠Dosáhne prvnÃhoyield. - Výsledek je vrácen vnÄjÅ¡Ãmu kódu.
- Druhé
.next(4)pÅedá4zpÄt generátoru jako výsledek prvnÃhoyielda obnovà jeho bÄh. - â¦BÄh dosáhne druhého
yield, které se stane výsledkem volánà generátoru. - TÅetÃ
.next(9)pÅedá do generátoru9jako výsledek druhéhoyielda obnovà jeho bÄh, který dosáhne konce funkce, takžedone: true.
Je to jako pingpong. Každé next(hodnota) (s výjimkou prvnÃho) pÅedá do generátoru hodnotu, která se stane výsledkem aktuálnÃho yield, a pak zÃská zpÄt výsledek dalÅ¡Ãho yield.
generátor.throw
Jak jsme vidÄli v uvedených pÅÃkladech, vnÄjšà kód může pÅedat generátoru hodnotu jako výsledek yield.
â¦Může v nÄm vÅ¡ak také vyvolat chybu. To je pÅirozené, neboÅ¥ chyba je druh výsledku.
Abychom pÅedali chybu do yield, mÄli bychom volat generátor.throw(chyba). V takovém pÅÃpadÄ je chyba vyvolána na Åádku s tÃmto yield.
NapÅÃklad zde vydánà "2 + 2 = ?" vede k chybÄ:
function* gen() {
try {
let výsledek = yield "2 + 2 = ?"; // (1)
alert("BÄh se sem nedostane, protože výše bude vyvolána výjimka");
} catch(e) {
alert(e); // zobrazà chybu
}
}
let generátor = gen();
let otázka = generátor.next().value;
generátor.throw(new Error("OdpovÄÄ nenalezena v mé databázi")); // (2)
Chyba, vhozená do generátoru na Åádku (2), povede k výjimce na Åádku (1) s yield. V uvedeném pÅÃkladu ji try..catch zachytà a zobrazÃ.
Pokud ji nezachytÃme, pak stejnÄ jako každá výjimka âvypadneâ z generátoru do volajÃcÃho kódu.
Aktuálnà Åádek volajÃcÃho kódu je Åádek s generátor.throw, oznaÄený (2). Tam ji tedy můžeme zachytit, napÅÃklad takto:
function* generuj() {
let výsledek = yield "2 + 2 = ?"; // Chyba na tomto Åádku
}
let generátor = generuj();
let otázka = generátor.next().value;
try {
generátor.throw(new Error("OdpovÄÄ nenalezena v mé databázi"));
} catch(e) {
alert(e); // zobrazà chybu
}
Pokud zde tuto chybu nezachytÃme, pak jako obvykle propadne do vnÄjÅ¡Ãho volajÃcÃho kódu (je-li nÄjaký), a nenÃ-li zachycena, shodà skript.
generátor.return
generátor.return(hodnota) ukonÄà bÄh generátoru a vrátà zadanou hodnotu hodnota.
function* gen() {
yield 1;
yield 2;
yield 3;
}
const g = gen();
g.next(); // { value: 1, done: false }
g.return('nic'); // { value: "nic", done: true }
g.next(); // { value: undefined, done: true }
Jestliže na dokonÄeném generátoru znovu použijeme generátor.return(), vrátà tuto hodnotu znovu (MDN).
Tuto metodu nepoužÃváme Äasto, protože vÄtÅ¡inou chceme zÃskat vÅ¡echny vracené hodnoty, ale může být užiteÄná, když chceme generátor zastavit za specifických podmÃnek.
ShrnutÃ
- Generátory se vytváÅejà generátorovými funkcemi
function* f(â¦) {â¦}. - UvnitÅ generátorů (pouze tam) existuje operátor
yield. - VnÄjšà kód a generátor si mohou vymÄÅovat výsledky pomocà volánÃ
next/yield.
V modernÃm JavaScriptu se generátory použÃvajà jen zÅÃdka, ale nÄkdy se mohou hodit, protože schopnost funkce vymÄÅovat si data s volajÃcÃm kódem pÅi jejÃm bÄhu je pomÄrnÄ unikátnÃ. A samozÅejmÄ jsou vynikajÃcà pro vytváÅenà iterovatelných objektů.
V dalšà kapitole se navÃc dozvÃme o asynchronnÃch generátorech, které se použÃvajà k naÄÃtánà proudů asynchronnÄ generovaných dat (napÅ. stránkovaných dat pÅi stahovánà ze sÃtÄ) v cyklech for await ... of.
PÅi programovánà webů pracujeme s datovými proudy Äasto, takže to je dalšà velmi důležitý pÅÃpad použitÃ.
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â¦)