Generátory

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, jinak false .

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

  • .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.

Generátory mohou generovat hodnoty navždy

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
  1. 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ího yield "2+2=?" . V tomto okamžiku generátor pozastaví provádění, zatímco zůstane na řádku (*) .
  2. Potom, jak je znázorněno na obrázku výše, výsledek yield dostane do question proměnná ve volacím kódu.
  3. Na generator.next(4) , generátor se obnoví a 4 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í:

  1. První .next() spustí provádění... Dosáhne prvního yield .
  2. Výsledek se vrátí do vnějšího kódu.
  3. Druhý .next(4) projde 4 zpět do generátoru jako výsledek prvního yield a obnoví provádění.
  4. ...Dosáhne druhého yield , který se stane výsledkem volání generátoru.
  5. Třetí next(9) projde 9 do generátoru jako výsledek druhého yield a obnoví provádění, které dosáhne konce funkce, takže done: 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í.