Generatoare
Generatoarele reprezintă un tip special de funcție folosite pentru generarea de valori. Pentru a defini generatoare, se utilizează simbolul asterisc (*) plasat după cuvântul cheie "function". De exemplu, să definim un generator simplu:
function* getNumber(){
yield 5;
}
const numberGenerator = getNumber();
const result = numberGenerator.next();
console.log(result); // {value: 5, done: false}
Aici, funcția getNumber() reprezintă un generator. Principalele aspecte ale creării și utilizării unui generator sunt:
- Generatorul este definit ca o funcție folosind operatorul function* (simbolul asterisc după cuvântul cheie function)
function* getNumber(){ .... }
Funcția generatorului returnează un iterator.
- Pentru a furniza valori din generator, așa cum se face în general în iterații, se utilizează operatorul yield, după care se specifică valoarea care urmează să fie returnată.
yield 5;
Deci, în acest caz, generatorul getNumber() efectiv generează numărul 5.
Pentru a obține valoarea din generator, se folosește operatorul next().
const result = numberGenerator.next();
În exemplul dat, apelând funcția getNumber() se creează un obiect iterator sub forma constantei numberGenerator. Utilizând acest obiect, putem obține valori din generator.
Dacă ne uităm la ieșirea în consolă, vom observa că acest metodă returnează următoarele date:
{value: 5, done: false}
Deci, esențial, se returnează un obiect ale cărui proprietăți includ value, care conține valoarea efectiv generată, și done, care indică dacă am ajuns la finalul generatorului.
Se poate observa că generatoarele seamănă cu iteratorii, dar, în esență, generatoarele reprezintă o formă specială de iteratori.
Acum, vom modifica codul:
function* getNumber(){
yield 5;
}
const numberGenerator = getNumber();
let next = numberGenerator.next();
console.log(next);
next = numberGenerator.next();
console.log(next);
Aici, apelul metodei next() are loc de două ori:
{value: 5, done: false}
{value: undefined, done: true}
Dar funcția generatorului getNumber generează doar o singură valoare - numărul 5. Prin urmare, la apelul repetat, proprietatea value va avea valoarea undefined, iar proprietatea done va fi true, adică lucrul generatorului s-a încheiat.
Un generator poate crea/genera mai multe valori:
function* getNumber(){
yield 5;
yield 25;
yield 125;
}
const numberGenerator = getNumber();
console.log(numberGenerator.next());
console.log(numberGenerator.next());
console.log(numberGenerator.next());
console.log(numberGenerator.next());
Ieșirea în consolă:
{value: 5, done: false}
{value: 25, done: false}
{value: 125, done: false}
{value: undefined, done: true}
Deci, la prima apelare a metodei next() din iterator, se extrage valoarea care urmează după primul operator yield, iar la a doua apelare a metodei next() - valoarea după al doilea operator yield și tot așa.
Pentru a simplifica, putem returna în generator elemente dintr-un array:
const numbers = [5, 25, 125, 625];
function* getNumber(){
for(const n of numbers){
yield n;
}
}
const numberGenerator = getNumber();
console.log(numberGenerator.next().value); // 5
console.log(numberGenerator.next().value); // 25
În acest context, este important să înțelegem că între două apeluri consecutive ale metodei next() poate trece o anumită perioadă nedeterminată, între ele pot avea loc alte acțiuni, și totuși generatorul va returna următoarea sa valoare.
const numberGenerator = getNumber();
console.log(numberGenerator.next().value); // 5
// alte acțiuni în șirul de operațiuni
console.log(numberGenerator.next().value); // 25
Generatorul nu trebuie neapărat să conțină doar definiții ale operatorilor yield. El poate include, de asemenea, logică mai complexă.
Cu ajutorul generatoarelor, este convenabil să creați secvențe infinite:
function* points(){
let x = 0;
let y = 0;
while(true){
yield {x:x, y:y};
x += 2;
y += 1;
}
}
let pointGenerator = points();
console.log(pointGenerator.next().value);
console.log(pointGenerator.next().value);
console.log(pointGenerator.next().value);
Ieșirea în consolă:
{x: 0, y: 0}
{x: 2, y: 1}
{x: 4, y: 2}
Întoarcerea dintr-un generator și funcția return
După cum am văzut mai devreme, fiecare apel ulterior al metodei next() returnează următoarea valoare din generator, însă putem încheia execuția generatorului folosind metoda return():
function* getNumber(){
yield 5;
yield 25;
yield 125;
}
const numberGenerator = getNumber();
console.log(numberGenerator.next()); // {value: 5, done: false}
numberGenerator.return(); // finisăm lucrul generatorului
console.log(numberGenerator.next()); // {value: undefined, done: true}
Obținerea valorilor din generator într-un ciclu
Deoarece pentru obținerea valorilor se folosește un iterator, putem utiliza bucla for...of:
function* getNumber(){
yield 5;
yield 25;
yield 125;
}
const numberGenerator = getNumber();
for(const num of numberGenerator){
console.log(num);
}
Ieșirea în consolă:
5
25
125
De asemenea, putem utiliza și alte tipuri de bucle, de exemplu, bucla while:
function* getNumber(){
yield 5;
yield 25;
yield 125;
}
const numberGenerator = getNumber();
while(!(item = numberGenerator.next()).done){
console.log(item.value);
}
Transmiterea datelor către generator
Funcția generatorului, la fel ca orice altă funcție, poate primi parametri. Prin urmare, prin intermediul parametrilor, putem transmite generatorului anumite date. De exemplu:
function* getNumber(start, end, step){
for(let n = start; n <= end; n +=step){
yield n;
}
}
const numberGenerator = getNumber(0, 8, 2);
for(num of numberGenerator){
console.log(num);
}
Aici, în funcția generatorului, sunt transmise valorile de start, stop și pas pentru numere. Ieșirea în consolă:
0
2
4
6
8
Încă un exemplu - să definim un generator care returnează date dintr-un array:
function* generateFromArray(items){
for(item of items)
yield item;
}
const people = ["Tom", "Bob", "Sam"];
const personGenerator = generateFromArray(people);
for(person of personGenerator)
console.log(person);
În acest caz, un array este transmis generatorului, iar acesta este utilizat pentru generarea valorilor. Ieșirea în consolă:
Tom
Bob
Sam
Transmiterea datelor în metoda next
Cu ajutorul next() se pot transmite date către generator. Datele transmise prin această metodă pot fi obținute în funcția generatorului prin intermediul prealabilului apel al operatorului yield:
function* getNumber(){
const n = yield 5; // primim valoarea numberGenerator.next(2).value
console.log("n:", n);
const m = yield 5 * n; / /primim valoarea numberGenerator.next(3).value
console.log("m:", m);
yield 5 * m;
}
const numberGenerator = getNumber();
console.log(numberGenerator.next().value); // 5
console.log(numberGenerator.next(2).value); // 10
console.log(numberGenerator.next(3).value); // 15
Ieșirea în consolă:
5
n: 2
10
m: 3
15
La a doua apelare a metodei next():
numberGenerator.next(2).value
Putem obține datele transmise prin intermediul acesteia atribuind rezultatul primului apel al operatorului yield:
const n = yield 5;
Deci, în acest caz, constanta n va fi egală cu 2, deoarece în metoda next() se transmite numărul 2.
Ulterior, putem utiliza această valoare, de exemplu, pentru a genera o nouă valoare:
const m = yield 5 * n;
Prin urmare, constanta m va primi valoarea transmisă prin al treilea apel al metodei next(), adică numărul 3.
Tratarea erorilor în generator
Cu ajutorul funcției throw() putem genera o excepție în generator. Ca parametru pentru această funcție, se transmite o valoare arbitrară care reprezintă informații despre eroare:
function* generateData(){
try {
yield "Tom";
yield "Bob";
yield "Hello Work";
}
catch(error) {
console.log("Error:", error);
}
}
const personGenerator = generateData();
console.log(personGenerator.next()); // {value: "Tom", done: false}
personGenerator.throw("Something wrong"); // Error: Something wrong
console.log(personGenerator.next()); // {value: undefined, done: true}
În primul rând, în funcția generatorului, pentru a trata o posibilă excepție, folosim construcția try..catch. În blocul catch, cu ajutorul parametrului "error", putem obține informații despre eroare, care sunt transmise prin funcția throw().
Ulterior, când utilizăm generatorul, putem apela această funcție, transmițându-i informații arbitrare despre eroare (în acest caz, este doar un mesaj de șir de caractere):
personGenerator.throw("Something wrong");
În final, această apelare va duce la generarea unei excepții în funcția generatorului, iar controlul va trece la blocul catch, care afișează informațiile despre eroare pe consolă:
catch(error) {
console.log("Error:", error);
}
Ieșirea în consolă a programului:
{value: "Tom", done: false}
Error: Something wrong
{value: undefined, done: true}
Este important să menționăm că după apelul funcției throw(), generatorul își încheie execuția, iar ulterior, la apelul metodei next(), vom obține rezultatul {value: undefined, done: true}.