ES6 a introdus un nou mod de a lucra cu funcții și iteratori sub forma generatoarelor (sau funcții generatoare). Un generator este o funcție care se poate opri la jumătatea drumului și apoi poate continua de unde s-a oprit. Pe scurt, un generator pare a fi o funcție, dar se comportă ca un iterator.
Fun Fact: async/await
se poate baza pe generatoare. Citiți mai multe aici.
Generatorii sunt legați în mod complicat de iteratori. Dacă nu știți despre iteratori, iată un articol care să vă ajute să îi înțelegeți mai bine.
Iată o analogie simplă pentru a avea o intuiție pentru generatori înainte de a trece la detalii tehnice.
Imaginați-vă că citiți un techno-thriller înțepător. Cu totul absorbit de paginile cărții, abia dacă auziți soneria de la ușă. Este băiatul care livrează pizza. Vă ridicați pentru a deschide ușa. Cu toate acestea, înainte de a face acest lucru, vă puneți un semn de carte la ultima pagină citită. Îți salvezi mental evenimentele din intrigă. Apoi, te duci să îți iei pizza. După ce te întorci în camera ta, începi cartea de la pagina la care ai pus semnul de carte. Nu o începi din nou de la prima pagină. Într-un anumit sens, ați acționat ca o funcție generatoare.
Să vedem cum putem utiliza generatoarele pentru a rezolva unele probleme comune în timpul programării. Dar, înainte de asta, să definim ce sunt generatoarele.
Ce sunt generatoarele?
O funcție normală, cum ar fi aceasta, nu poate fi oprită înainte de a-și termina sarcina, adică de a fi executată ultima sa linie. Ea urmează ceva ce se numește modelul run-to-completion.
function normalFunc() {
console.log('I')
console.log('cannot')
console.log('be')
console.log('stopped.')
}
Singurul mod de a ieși din normalFunc
este prin return
ieșirea din el, sau throw
ieșirea unei erori. Dacă apelați din nou funcția, aceasta va începe din nou execuția de la început.
În schimb, un generator este o funcție care se poate opri la jumătatea drumului și apoi să continue de unde s-a oprit.
Iată alte câteva definiții comune ale generatoarelor –
- Generatoarele sunt o clasă specială de funcții care simplifică sarcina de a scrie iteratori.
- Un generator este o funcție care produce o secvență de rezultate în loc de o singură valoare, adică generează o serie de valori.
În JavaScript, un generator este o funcție care returnează un obiect asupra căruia puteți apela next()
. Fiecare invocare a lui next()
va returna un obiect de forma –
{
value: Any,
done: true|false
}
Proprietatea value
va conține valoarea. Proprietatea done
este fie true
, fie false
. Când done
devine true
, generatorul se oprește și nu va mai genera alte valori.
Iată o ilustrare a aceluiași lucru –
Rețineți săgeata punctată care închide bucla randament-rezumat-randament chiar înainte de Finish în partea Generators din imagine. Există posibilitatea ca un generator să nu se termine niciodată. Vom vedea un exemplu mai târziu.
Crearea unui generator
Să vedem cum putem crea un generator în JavaScript –
function * generatorFunction() { // Line 1
console.log('This will be executed first.');
yield 'Hello, '; // Line 2 console.log('I will be printed after the pause');
yield 'World!';
}const generatorObject = generatorFunction(); // Line 3console.log(generatorObject.next().value); // Line 4
console.log(generatorObject.next().value); // Line 5
console.log(generatorObject.next().value); // Line 6// This will be executed first.
// Hello,
// I will be printed after the pause
// World!
// undefined
Concentrează-te pe părțile îngroșate. Pentru a crea o funcție de generator, folosim sintaxa function *
în loc de doar function
. Între cuvântul cheie function
, *
și numele funcției poate exista orice număr de spații. Deoarece este doar o funcție, o puteți folosi oriunde poate fi folosită o funcție, adică în interiorul obiectelor și în metodele claselor.
În interiorul corpului funcției, nu avem un return
. În schimb, avem un alt cuvânt cheie yield
(linia 2). Este un operator cu ajutorul căruia un generator se poate pune în pauză. De fiecare dată când un generator întâlnește un yield
, acesta „returnează” valoarea specificată după el. În acest caz, se returnează Hello,
. Cu toate acestea, noi nu spunem „returnat” în contextul generatoarelor. Spunem că „generatorul a dat Hello,
„.”
De asemenea, ne putem întoarce de la un generator. Cu toate acestea, return
setează proprietatea done
la true
, după care generatorul nu mai poate genera alte valori.
function * generatorFunc() {
yield 'a';
return 'b'; // Generator ends here.
yield 'a'; // Will never be executed.
}
În linia 3, creăm obiectul generator. Se pare că invocăm funcția generatorFunction
. Într-adevăr, așa facem! Diferența este că, în loc să returneze orice valoare, o funcție de generator returnează întotdeauna un obiect generator. Obiectul generator este un iterator. Deci îl puteți utiliza în buclele for-of
sau în alte funcții care acceptă un iterabil.
În linia 4, apelăm metoda next()
pe generatorObject
. Cu acest apel, generatorul începe să se execute. În primul rând, acesta console.log
pe This will be executed first.
Apoi, întâlnește un yield 'Hello, '
. Generatorul cedează valoarea sub forma unui obiect { value: 'Hello, ', done: false }
și se suspendă/se oprește. Acum, acesta așteaptă următoarea invocare.
În linia 5, apelăm din nou next()
. De data aceasta, generatorul se trezește și începe execuția de unde a rămas. Următoarea linie pe care o găsește este un console.log
. Acesta înregistrează șirul I will be printed after the pause
. Se întâlnește un alt yield
. Valoarea este redată sub forma obiectului { value: 'World!', done: false }
. Extragem proprietatea value
și o înregistrăm. Generatorul doarme din nou.
În linia 6, invocăm din nou next()
. De data aceasta nu mai sunt linii de executat. Amintiți-vă că fiecare funcție returnează implicit undefined
dacă nu este furnizată o instrucțiune return. Prin urmare, generatorul returnează (în loc să cedeze) un obiect { value: undefined, done: true}
. done
este setat la true
. Acest lucru semnalează sfârșitul acestui generator. Acum, acesta nu mai poate genera alte valori sau relua din nou, deoarece nu mai există alte instrucțiuni care să fie executate.
Va trebui să creăm un nou obiect generator pentru a rula din nou generatorul.
Utilizări ale generatoarelor
Există multe cazuri impresionante de utilizare a generatoarelor. Să vedem câteva dintre ele.
Implementarea Iterabilelor
Când implementați un iterator, trebuie să faceți manual un obiect iterator cu o metodă next()
. De asemenea, trebuie să salvați manual starea. De multe ori, devine foarte greu de făcut acest lucru. Deoarece generatoarele sunt, de asemenea, iterabile, acestea pot fi utilizate pentru a implementa iterabile fără codul suplimentar de tip boilerplate. Să vedem un exemplu simplu.
Problemă: Vrem să facem un iterabil personalizat care să returneze This
, is
și iterable.
. Iată o implementare folosind iteratori –
const iterableObj = {
() {
let step = 0;
return {
next() {
step++;
if (step === 1) {
return { value: 'This', done: false};
} else if (step === 2) {
return { value: 'is', done: false};
} else if (step === 3) {
return { value: 'iterable.', done: false};
}
return { value: '', done: true };
}
}
},
}for (const val of iterableObj) {
console.log(val);
}// This
// is
// iterable.
Iată același lucru folosind generatoare –
function * iterableObj() {
yield 'This';
yield 'is';
yield 'iterable.'
}for (const val of iterableObj()) {
console.log(val);
}// This
// is
// iterable.
Puteți compara ambele versiuni. Este adevărat că acesta este un exemplu oarecum artificial. Dar ilustrează punctele –
- Nu trebuie să ne facem griji cu privire la
Symbol.iterator
- Nu trebuie să implementăm
next()
. - Nu trebuie să realizăm manual obiectul de întoarcere al lui
next()
, adică{ value: 'This', done: false }
. - Nu trebuie să salvăm starea. În exemplul iteratorului, starea a fost salvată în variabila
step
. Valoarea acesteia a definit ceea ce a ieșit din iterabil. Nu a trebuit să facem nimic de acest fel în generator.
Funcționalitate asincronă mai bună
Codul care utilizează promisiuni și callback-uri, cum ar fi –
function fetchJson(url) {
return fetch(url)
.then(request => request.text())
.then(text => {
return JSON.parse(text);
})
.catch(error => {
console.log(`ERROR: ${error.stack}`);
});
}
poate fi scris ca (cu ajutorul unor biblioteci precum co.js)-
const fetchJson = co.wrap(function * (url) {
try {
let request = yield fetch(url);
let text = yield request.text();
return JSON.parse(text);
}
catch (error) {
console.log(`ERROR: ${error.stack}`);
}
});
Câțiva cititori au observat că este paralelă cu utilizarea lui async/await
. Aceasta nu este o coincidență. async/await
poate urma o strategie similară și înlocuiește randamentul cu await
în cazurile în care sunt implicate promisiuni. Se poate baza pe generatoare. Vedeți acest comentariu pentru mai multe informații.
Curente de date infinite
Este posibil să se creeze generatoare care nu se termină niciodată. Luați în considerare acest exemplu –
function * naturalNumbers() {
let num = 1;
while (true) {
yield num;
num = num + 1
}
}const numbers = naturalNumbers();console.log(numbers.next().value)
console.log(numbers.next().value)// 1
// 2
Creăm un generator naturalNumbers
. În interiorul funcției, avem o buclă infinită while
. În acea buclă, yield
îl num
. Când generatorul cedează, acesta este suspendat. Când apelăm din nou next()
, generatorul se trezește, continuă de unde a fost suspendat (în acest caz yield num
) și se execută până când se întâlnește un alt yield
sau generatorul se termină. Deoarece următoarea instrucțiune este num = num + 1
, aceasta actualizează num
. Apoi, se merge în partea de sus a buclei while. Condiția este încă adevărată. Se întâlnește următoarea linie yield num
. Acesta produce num
actualizat și se suspendă. Acest lucru continuă atâta timp cât doriți.
Generatoarele ca observatori
Generatoarele pot, de asemenea, să primească valori folosind funcția next(val)
. Atunci generatorul se numește observator, deoarece se trezește atunci când primește valori noi. Într-un anumit sens, el continuă să observe pentru valori și acționează atunci când primește una. Puteți citi mai multe despre acest model aici.
Avantajele generatoarelor
După cum s-a văzut în exemplul Infinite Data Streams, acesta este posibil doar datorită evaluării leneșe. Evaluarea leneșă este un model de evaluare care amână evaluarea unei expresii până când este nevoie de valoarea sa. Adică, dacă nu avem nevoie de valoare, aceasta nu va exista. Ea este calculată pe măsură ce o solicităm. Să vedem un exemplu –
function * powerSeries(number, power) {
let base = number;
while(true) {
yield Math.pow(base, power);
base++;
}
}
La powerSeries
se dă seria numărului ridicat la o putere. De exemplu, seria de puteri a lui 3 ridicată la 2 ar fi 9(3²) 16(4²) 25(5²) 36(6²) 49(7²). Când facem const powersOf2 = powerSeries(3, 2);
nu facem decât să creăm obiectul generator. Niciuna dintre valori nu a fost calculată. Acum, dacă apelăm next()
, 9 va fi calculat și retuned.
Eficientă din punct de vedere al memoriei
O consecință directă a evaluării leneșe este că generatoarele sunt eficiente din punct de vedere al memoriei. Generăm doar valorile care sunt necesare. Cu funcțiile normale, trebuia să generăm în prealabil toate valorile și să le păstrăm la îndemână în cazul în care le folosim mai târziu. Cu toate acestea, cu generatoarele, putem amâna calculul până când avem nevoie de el.
Putem crea funcții combinatoare pentru a acționa asupra generatoarelor. Combinatorii sunt funcții care combină iterabilele existente pentru a crea altele noi. un astfel de combinator este take
. Acesta ia primele n
elemente ale unui iterabil. Iată o implementare –
function * take(n, iter) {
let index = 0;
for (const val of iter) {
if (index >= n) {
return;
}
index = index + 1;
yield val;
}
}
Iată câteva cazuri interesante de utilizare a lui take
–
take(3, )// a b ctake(7, naturalNumbers());// 1 2 3 4 5 6 7take(5, powerSeries(3, 2));// 9 16 25 36 49
Iată o implementare a bibliotecii ciclate (fără funcționalitatea de inversare).
function * cycled(iter) {
const arrOfValues =
while (true) {
for (const val of arrOfValues) {
yield val
}
}
}console.log(...take(10, cycled(take(3, naturalNumbers()))))// 1 2 3 1 2 3 1 2 3 1
Caveți
Există câteva puncte pe care ar trebui să le rețineți în timpul programării cu ajutorul generatoarelor.
- Obiectele generatoare sunt accesate doar o singură dată. Odată ce ați epuizat toate valorile, nu mai puteți itera din nou peste ele. Pentru a genera din nou valorile, trebuie să creați un nou obiect generator.
const numbers = naturalNumbers();console.log(...take(10, numbers)) // 1 2 3 4 5 6 7 8 9 10
console.log(...take(10, numbers)) // This will not give any data
- Obiectele generator nu permit accesul aleatoriu, așa cum este posibil cu array-urile. Deoarece valorile sunt generate una câte una, accesarea unei valori aleatoare ar duce la calcularea valorilor până la elementul respectiv. Prin urmare, nu este vorba de acces aleatoriu.
Concluzie
În generatoare sunt încă multe lucruri de acoperit. Lucruri precum yield *
, return()
și throw()
. Generatoarele fac, de asemenea, posibile corutinele. Am enumerat câteva referințe pe care le puteți citi pentru a înțelege mai bine generatoarele.
Vă puteți îndrepta către pagina itertools a lui Python și puteți vedea câteva dintre utilitățile care permit lucrul cu iteratori și generatoare. Ca un exercițiu, puteți implementa singuri utilitățile.
.