JavaScript iteratori i generatori

Jedna od novosti uvedenih u ECMAScript 5 standardu su protokoli iteracije. U pitanju su iterable i iterator protokoli.

Iteracije smo upoznali prilikom rada sa nizovima. Poznato nam je da nizovi, odnosno Array objekat ima metode za prolazak kroz elemente niza. Pri tome, programer definiše callback funkciju koja se poziva pri svakoj iteraciji, odnosno za svaki elemenat niza. Uvođenjem iteratora, za bilo koji objekat možemo definisati prolazak kroz njegove podatke putem "uzastopnih upita", odnosno iteracija.

U ovom tekstu baratamo sa nekoliko sličnih termina - pazite da se ne zbunite. Evo "rečnika":

Iterable protokol omogućava programeru da definiše ponašanje nekog objekta prilikom iterativnog poziva. Drugim rečima, moguće je definisati iterativno ponašanje i podatke koje objekat vraća prilikom svake iteracije, čak i za objekte koji inače nemaju ovo ponašanje ugrađeno. Isto tako, moguće je promeniti rezultate iteracije i onim objektima koji već imaju ovo ponašanje.

Objekti kroz koje je moguće vršiti iteracije imaju (ili nasleđuju) interni @@iterator metod. Ovom metodu ne pristupamo direktno, već preko konstante Symbol.iterator. Ovo svojstvo objekta je referenca na iteratorsku funkciju.

obj[Symbol.iterator] = function() {...};

Iteratorska funkcija je specifična samo po tome što vraća rezultat prema iterator protokolu. Ovaj protokol prosto zahteva da najpre funkcija kreira objekat koji zovemo "iterator" i takođe služi da obezbedi closure za iteratorov metod next() koji se poziva u svakoj iteraciji.

Dakle iterator ima obavezno makar metod next(), od koga se očekuje da pri svakom pozivu obezbedi (nov) rezultat u tačno određenoj formi. Drugim rečima, podatak koji tražimo u svakoj iteraciji, ne dobijamo samo tako, kao "suv podatak" već se nalazi "spakovan" unutar objekta, koji u stvari predstavlja vraćeni rezultat metoda next(). Ovaj objekat ima dva svojstva:

function iteratorska_funkcija() { ... // iteratorska funkcija vraća iterator return { next: function() { // iteratorov metod next() vraća objekat sa podatkom return { done: logička_vrednost, value: podatak }; } }; }

Kada objektu definišemo ovakav metod i povežemo ga preko Symbol.iterator konstante, učinili smo taj objekat "iterabilnim". Kako se onda koristi iterator?

Na početku pozivamo iteratorsku funkciju, koja kao rezultat vraća objekat sa next() metodom. Uzastopnim pozivanjem next() metoda ovog objekta dobijamo jedan po jedan podatak "umotan" u objekat koji nas obaveštava da li smo završili sa prolaskom kroz podatke i ako nismo, koja je vrednost samog podatka.

Primer - Iteratori

Ovde ćemo demonstrirati kako jednom sasvim običnom objektu dodajemo iterator koji "izbacuje" podatak za podatkom, kako mi želimo. Najpre ćemo kreirati objekat sa dva niza. Nizovi ne moraju da imaju isti broj elemenata. Želimo da definišemo iteracije kroz objekat gde bi kroz svaku iteraciju bila vraćena aritmetička sredina svakog para elemenata iz ovih nizova.


  // kreiramo neki objekat
  let obj = {
    valuesHigh: [89, 134, 111, 96, 102, 99], 
    valuesLow: [34, 22, 52, 47, 61]
  };
  
  // objektu dodajemo iteratorsku funkciju
  obj[Symbol.iterator] = function() {
    let _self = this;
    let i = -1;
    let N = Math.min(this.valuesHigh.length, this.valuesLow.length);
    return {
      next: function() {
        i++;
        if (i<N)
          return { value: (_self.valuesHigh[i] + _self.valuesLow[i]) /2 };
        else 
          return { done: true };
      }
    };
  };
  
  // dobijamo iterator objekat
  let iterator = obj[Symbol.iterator]();
  
  // najzad, same iteracije
  console.log( iterator.next().value );  // 61.5 = (89+34)/2
  console.log( iterator.next().value );  // 78
  // ...
  console.log( iterator.next().value );  // undefined

Najpre kreiramo osnovni objekat obj sa podacima. Podaci su smešteni u dva niza obj.valuesHigh i obj.valuesLow. Zatim objektu dodajemo iteratorski metod. Metod postavlja početne vrednosti promenljivih i i N, i vrši povezivanje samog objekta preko _self reference. Na ovaj način se obezbeđuje pristup podacima objekta preko closure mehanizma.

Onda dolazi glavna stvar - iteratorska funkcija kao rezultat vraća iterator - objekat koji ima samo jedan metod, a to je next(). Unutar ovog metoda programiramo prelazak na sledeći indeks i, proveru da li smo stigli do kraja sa podacima i vraćanje rezultata svake iteracije. Vidimo da postoje dve varijante - kada podatak postoji i kada više nema podataka. U prvom slučaju rezultat je objekat sa svojstvom value koji sadrži sam podatak, a u drugom objekat sa svojstvom done setovanim na true.

Sada, kada je objekat kompletiran, ništa nas ne sprečava da započenmo iteracije. Postavljanje "brojača" na početne vrednosti, tj. započinjanje iteracija iz početka obavlja se kreiranjem iterator objekta, putem poziva iteratorske funkcije. Ovo je objekat koji ima next() metodu za pristup svakom podatku što onda i koristimo za ispis podataka u konzoli. Ako dovoljno puta pozovemo iteraciju (u ovom slučaju 5), na kraju ćemo dobiti undefined kao rezultat - tada smo dobili objekat koji ima done, a ne value svojstvo.

Dodavanje iterator metoda nismo morali da uradimo posle definicije objekta, već i u samom objektnom literalu, na sledeći način:


  let obj = {
    valuesHigh: [89, 134, 111, 96, 102, 99], 
    valuesLow: [34, 22, 52, 47, 61],
    [Symbol.iterator]: function() { ... }
  };

Isprobajte kako funkcioniše ovaj primer.

js-fun-iterator-2

Ciklus for..of

Kada želimo da iterativno prođemo kroz neki objekat, možemo koristiti specijalan tip ciklusa for..of. Za razliku od for..in ciklusa koji nam služi da "nabrojimo" svojstva nekog objekta, for..of petlju možemo koristiti samo nad objektom koji ima definsano iterativno ponašanje.

Na primer, nizovi i stringovi su po defaultu iterable objekti, pa tako nad njima možemo koristiti i for..of ciklus. Rezultati, odnosno podaci koje dobijamo u ovom ciklusu će zavisiti od samog objekta. Array objekat, po definiciji, kroz svaku iteraciju vraća po jedan elemenat niza. Sa druge strane, objekat String kroz svaku iteraciju vraća po jedan znak stringa. Evo kako se zadaje for..of petlja:

for (podatak of objekat) { ... }

Pri svakom prolasku kroz ciklus, promenljiva podatak dobija vrednost podatka koji se vraća kroz iterator. Ovde ne moramo da vodimo računa o pozivanju iteratorske funkcije, iteratoru, next() metodi, niti njenoj povratnoj vrednosti - ne zanimaju nas ni svojstva value ni done. Sve se obavlja automatski.

Kada koristimo for..of petlju, vrši se automatska inicijalizacija iteracija. To u stvari znači da ne moramo da pozivamo iteratorsku funkciju objekta kako bismo "izvukli" iteratorski objekat.

Ako iz nekog razloga prekinemo for..of petlju (npr. korišćenjem direktive break), kasnije možemo ponovo pokrenuti for..of, ali iteracije se neće nastaviti, već početi od početka.

Primer - For..of petlja

Evo jednog načina kako bismo ispisali sve podatke koje dobijamo putem iteracija kroz neki objekat, na "klasičan" način:


  let iterator = obj[Symbol.iterator]();
  
  do {
    let rezultat = iterator.next();
    if (!rezultat.done)
      console.log( rezultat.value );
  } while (!rezultat.done);

To nije bilo baš elegantno, je l' da? Korišćenjem for..of ciklusa, sve je mnogo jednostavnije:


  for (let podatak of obj) {
    console.log( podatak );
  }

Generatorska funkcija

U ECMAScript 6 verziji JavaScripta, još je više urađeno po pitanju olakšavanja dodavanja iterabilnosti objektu. Vidite, uvođenje funkcije iteratora može brzo da se iskomplikuje i programeri lako prave greške kod formiranja rezultata.

Upravo da bi se programerima "olakšao život", uvedene su generatorske funkcije. Njihova primarna uloga je uvođenje iterativnog ponašanja, ali se u stvari mogu koristiti u različitim situacijama.

Generatorska funkcija može prekinuti svoje izvršavanje i kasnije ga nastaviti "po pozivu". Tako ova funkcija može raditi "u nastavcima". Interpreter zna da je neka funkcija generatorska, ako je obeležena simbolom * (zvezdicom).

DEKLARACIJA FUNKCIJE: function* fun(parametri) {...} FUNKCIJSKI IZRAZ: function* (parametri) {...} METOD OBJEKTA: ..., fun: function* (parametri) {...} METOD OBJEKTA: ..., * fun(parametri) {...}

Kako funkcioniše ta stvar sa prekidanjem i nastavljanjem izvršavanja funkcje? Svi znamo da funkcija vraća rezultat direktivom return. Međutim, u generatorskim funkcijama se može koristiti i specijalna direktiva yield, koja takođe vraća vrednost i prekida rad funkcije, ali isto tako predstavlja tačku kasnijeg povrataka i nastavka rada funkcije.

To znači da u generatorskoj funkciji možemo imati više puta zadatu direktivu yield. Pošto suštinski generatorska funkcija predstavlja poseban tip iteratorske funkcije, i ona funkcioniše po iterator protokolu. To znači da pozivanjem generatorske funkcije dobijamo generator objekat (koji je praktično specijalna verzija iteratora), sve sa next() metodom koju možemo uzastopno pozivati. Svaki generator.next() poziv nas "vraća" u generatorsku funkciju na poziciju posle poslednje yield direktive.

function* generatorska_funkcija(parametri) { ... yield vrednost; ... yield vrednost; ... } let generator = generatorska_funkcija(parametri); generator.next().value; generator.next().value; ...

Kao i svaka druga funkcija, i generatorska može imati direktivu return, s tim što posle izvršavanja return, nema više vraćanja u funkciju - to znači da je generator objekat "odsvirao svoje". Isto važi i za prirodno stizanje do kraja generatorske funkcije. U oba slučaja će svaki sledeći poziv metoda next() vraćati vrednost undefined.

Osim next(), generator ima još dva metoda za poziv, odnosno povratak u generatorsku funkciju. To su return() i throw().

Metod return() nas izbacuje iz generatora, kao da je u funkciji momentalno naleteo na return. Vraća objekat {done:true, value:undefined}. Posle toga, nema više iteracija.

Metod throw() radi to isto, s tim što ne vraća rezultat već podiže izuzetak (exception), kao da smo naleteli na grešku u izvršavanju programa pri pozivu metoda next().

Kao da to nije dosta, metod next() iteratorskog objekta može imati i neku vrednost zadatu kao parametar. Ova vrednost se tada prosleđuje generatoru na mestu gde se nalazio poslednji izvršeni yield, što onda postaje vrednost yield izraza. Deluje komplikovano? Pogledajte sliku, biće jasnije.

Funkcionisanje generatora u JavaScriptu
Funkcionisanje generatora i generatorske funkcije
  1. Na početku pozivamo generatorsku funkciju gen().
  2. Rezultat tog poziva je generator objekat, na koji će pokazivati referenca itr.
  3. Sada koristimo metod next() objekta itr kako bismo izvršili prvi deo koda iz generatorske funkcije.
  4. Kada naiđe na yield, "iskače" iz funkcije, a kao rezultat se vraća vrednost v1, koja se nalazi unutar objekta i to pod svojstvom value, što se smešta u promenljivu x.
  5. Ponovo pozivamo metod next() generatora, čime se vraćamo u generatorsku funkciju i nastavljamo sa drugim delom koda.
  6. Kada stigne do sledećeg yield, pnovo će prekinuti izvršavanje funkcije i vratiće vrednost v2, koja će na kraju završiti u promenljivoj y.
  7. Ponovo pozivamo metod next() kako bismo nastavili izvršavanje generatora, ali ovaj put prosleđujemo i neku vrednost v, koja će predstavljati vrednost poslednjeg yield izraza i biće smeštena u lokalnu promenljivu p, da bi se odmah potom nastavilo izvršavanje trećeg dela funkcije.
  8. Ponovo nailazi na yield, prekida funkciju i vraća vrednost v3, koja se smešta u promenljivu z.
  9. Poslednji put pozivamo metod next(), da bi se vratio u funkciju i izvršio četvrti deo koda.
  10. Konačno, generatorska funkcija vraća poslednju vrednost v4 direktivom return, koja se smešta u promenljivu w. Posle return (ili kad se stigne do kraja funkcije) nema više povratka u funkciju, gotovo je sa iteracijama. Svaki sledeći poziv next() metode vratiće vrednost undefined.

Pogledajte kako funkcioniše upravo ovaj primer sa slike.

js-fun-generator-1

Inače, sve ovo važi i za metode return() i throw(), s tim što prosleđena vrednost kod return() predstavlja i vrednost koja će biti vraćena, dok kod throw(), prosleđena vrednost predstavlja vrednost objavljene greške, tj. izuzetka.

Primer - Generatori

Jedan jednostavan generator

Ako ćemo pravo, možemo koristiti generatore i "na suvo", tj. bez uvođenja iterabilnosti nekom objektu. U ovom primeru smo napravili generator koji beskonačno izbacuje brojeve Fibonačijevog niza gde se svaki sledeći broj dobija kao zbir prethodna dva (1, 1, 2, 3, 5, 8, 13, itd).

Zatim pozivamo generatorsku funkciju da bismo dobili generator objekat i svaki put kad nam zatreba, preko tog objekta "izvlačimo" sledeći broj niza.


  // generatorska funkcija
  function* fibGen() {
    let a = 0;
    let b = 1;
    while (true) {
      let x = a+b;
      a = b;
      b = x;
      yield a;
    }
  }

  // kreiranje i korišćenje generatora
  let fib = fibGen();
  for (let i=0; i<10; i++)
    console.log(fib.next().value); // 1,1,2,3,5, ... 55

  console.log(fib.next().value);  // 89 - kad god poželimo, dobijamo sledeću vrednost

  console.log(fib.return("Gotovo").value);  // "Gotovo" - zatvaramo generator
  
  console.log(fib.next().value);  // undefined

Vidimo da u generatorskoj funkciji fibGen() zaista imamo beskonačni ciklus while(true) {...}. Zašto se onda web čitač ne "zaglavi" izvršavajući mrtvu petlju? Jednostavno - unutar petlje imamo yield kojim vraćamo vrednost (tj. sledeći izračunati broj Fibonačijevog niza). Kao što smo naučili, yield vraća vrednost, slično kao return, s tim što se izvršavanje funkcije tu ne prekida, već pauzira. Ciklus se neće izvršavati dok se funkcija ne nastavi, a nastviće se tek po sledećem "zahtevu" za novim brojem.

U primeru vidimo i poziv funkcije fibGen(), s tim što kod generatorske funkcije poziv ne znači izvršavanje, već samo dobijanje generator objekta koji smo u ovom slučaju vezali za referencu fib. Svaku sledeću vrednost izvlačimo pozivanjem metoda fib.next() koji opet vraća objekat u kome je spakovana sama vrednost (uz informaciju da li smo stigli do kraja iteracija). Znači yield izraz vraća objekat, a podatak koji želimo se krije u svojstvu value tog objekta.

Dakle kroz ciklus izvlačimo prvih 10 brojeva Fibonačijevog niza, a i kad god kasnije pozovemo fib.next() dobijamo sledeću vrednost. Ako želimo da završimo sa iteracijama, umesto next() možemo pozvati metod fib.return(), koji prosto završava rad generatora i umesto sledećeg broja vraća vrednost koju smo prosledili kao parametar. Da nije bilo parametra, vrednost bi bila undefined. Ako posle ovog poziva opet pokušamo da izvučemo neki broj pozivanjem fib.next(), nećemo uspeti - iteracije su gotove.

js-fun-generator-2

Uvođenje iterabilnosti objekta pomoću generatora

Da bi se držali teme, ovde ćemo predstaviti korišćenje generatorske funkcije u cilju dodavanja iterabilnog ponašanja nekom objektu. Ovaj kod možete uporediti sa primerom u kome smo koristili iteratorsku funkciju. Primetićete koliko je ovakav program čistiji i jednostavniji.


  // kreiramo neki objekat
  let obj = {
    valuesHigh: [89, 134, 111, 96, 102, 99], 
    valuesLow: [34, 22, 52, 47, 61]
  };
  
  // objekat postaje iterabilan preko generatorske funkcije
  obj[Symbol.iterator] = function*() {
    let N = Math.min(this.valuesHigh.length, this.valuesLow.length);
    for (let i=0; i<N; i++) {
      yield (this.valuesHigh[i] + this.valuesLow[i]) /2;
    }
  };

  // kao i sa iteratorom, možemo iščitavati podatke u for..of petlji
  for (let podatak of obj) {
    console.log( podatak );
  }

Naravno, ako baš želimo, možemo preuzimati podatke i preko generator objekta, kao što smo radili i ranije.


  // isto tako smo mogli i "ručno" da čitamo preko generator objekta
  let generator = obj[Symbol.iterator]();
  
  console.log( generator.next().value );  // 61.5 = (89+34)/2
  console.log( generator.next().value );  // 78
  // ...
  console.log( generator.next().value );  // undefined

I ovaj kod možete slobodno isprobati.

js-fun-generator-3
  1. Mozilla Developer Network, Iterators and generators
  2. Mozilla Developer Network, for...of
  3. Exploring ES6, Iterables and iterators
  4. Exploring ES6, Generators
Svi elementi sajta Web'n'Study, osim onih za koje je navedeno da su u javnom vlasništvu, vlasništvo su autora i ne smeju se koristiti, u celosti ili delimično bez pismenog odobrenja autora. To uključuje tekstove, slike, ilustracije, animacije, prateći grafički materijal i programski kod.
Ovaj sajt koristi tehnologiju kolačića (cookies). Detaljnije o tome možete pročitati u tekstu o našoj politici privatnosti.