Iterable protokol
Karakteristike protokola
“Iterable protokol” je dogovoren APi na nivou JavaScript-a kao jezika, za kreiranje objekata koji se mogu koristiti za iteraciju nad kolekcijom podataka. Ključna karakteristika ovoga protokola je “sekvencijalanost” tj. osobina da pri iteraciji vraća vrednost iterabilne strukture jednu po jednu. Tipovi podataka koji zadovoljavaju iterable protokol se nazivaju “iterable kolekcije”. Poštujući iterable protokol potrebno je da funkcija vraća objekat koji sadrži specijalnu metodu “next()”.
next()
Metoda next() pri pozivanju vraća člana kolekcije privremeno “obmotanog” (eng. wrapped”) sa objektom koji ima dva svojstva: value i done. Dokle god postoje članovi za iteraciju, metoda vraća vrednost svojstsva “done” kao”false”, a tek kada iteracija “dodje” do kraja, svojstvo “done” se definiše kao “true”.
Primer
Kroz naredni primer je opisana koncepcija protokola:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function arrayIterator(array) { var i = 0; return { next: function() { return i < array.length ? { value: array[i++], done: false } : { done: true }; } } } |
Koji su tipovi podataka iterabilni?
Treba nagalasiti da obični javascript objekti nisu iterabilini, ali zato sledeći tipovi podataka zadovoljavaju iterable protokol:
- String
- Array
- Array-like argumenti ili NodeList objekat
- TypedArray
- Map
- Set
Koji mehanizmi u JavaScriptu zahtevaju iterable kolekciju?
Mehanizmi za čije korišćenje su neophodne “iterable” kolekcije su:
- Destructuring
- for..of loop
- Array.from()
- Spread operator (…)
- Maps & Sets
- Promise.all(), Promise.race()
- yield*
Primer
Iterable kolekcije mogu koristi mehanizme za koje su potrebni iterable kolekcije kao npr. iteracija sa “for..of” petljom:
1 2 3 4 |
const array = [1, 2, 3, 4]; for(let elem of array) { console.log(elem); } |
ili pri destruktuiranju:
1 2 3 4 5 6 7 |
const nekaMapa = new Map([['key1', 1], ['key2', 2], ['key3', 3]]); for(let [key, value] of nekaMapa) { console.log(`${key}: ${value}`); } |
Iterable i Iterator
Kreiranje iteratora sa iterable objektom
Za kreiranje iteratora je potrebno da objekat ima specifični interni metod koji vraća iterator, a čiji ključ je “Symbol.iterator”. Takav objekat se naziva “Iterable objekat”.
1 2 3 4 5 6 7 |
{ [Symbol.iterator]() { // Telo funkcije koje vraća iterator objekat } } |
Iterator objekat nastaje pozivajući metodu iterable objekta pod nazivom: “[Symbol.iterator]( )”.
1 |
const noviIterator = nekiIterableTipPodataka[Symbol.iterator](); |
Definicija
Iterator je objekat, koji zna kako da pristupi članovima iterable kolekcije, jedan po jedan i sadrži pointer koji ukazuje na sledeći element. Iterator objekat obezbedjuje “next()” metodu, koja vraća člana kolekcije “obmotanog” (eng. wrapped”) sa objektom koji ima dva svojstva: value i done.
Primer
1 2 3 4 5 6 7 8 9 10 11 |
// Iterabilna struktura podataka const nekiNiz = ['a', 'b', 'c']; // Kreiranje Iterator objekta sa iterable metodom koristeći "Symbol.iterator" ključ const nekiIterator = nekiNiz[Symbol.iterator](); // Pristupanje članovima itrable kolekcije nekiIterator.next() // { value: 'a', done: false } nekiIterator.next() // { value: 'b', done: false } nekiIterator.next() // { value: 'c', done: false } nekiIterator.next() // { value: undefined, done: true } |
Kao što se iz prethodnog primera vidi da svojstvo “done” signalizira kada je iteracije došla do kraja.
Primena Iterable i Iteratora
Zahvaljujući iteratoru i metodi next() je omogućena iteracija kroz iterabilnu kolekciju za for..of petljom:
1 2 3 4 5 6 7 8 9 10 11 12 |
// Iterabilna struktura podataka const nekiNiz = ['a', 'b', 'c']; // Kreiranje Iterator objekta sa iterable metodom koristeći "Symbol.iterator" ključ const nekiIterator = nekiNiz[Symbol.iterator](); for (let n of nekiIterator) { console.log(n) } // a // b // c |
Generatorska funkcija i Generatori
Obeležavanje generatorske funkcije
Generatorska funkcija je specijalni tip funkcije koja se obeležava na sledeći način:
1 2 3 |
function* nazivFunkcije (){ ... } |
Generatorska funkcija može da se definiše i kao metoda objekta:
1 2 3 4 5 |
const obj = { * generatorMethod() { ··· } }; |
ili čak kao metoda klase:
1 2 3 4 5 |
class MyClass { * generatorMethod() { ··· } } |
Karakteristike generatorske funkcije
Generatorska funkcija se ponaša kao “factory” za specifične iteratore tzv. generatori, a pored toga može pauzirati svoj rad sa ključnom reči “yield”. Kada kompajler pri izvršavanju funkcije “naleti” na ključnu reč “yield” pauzira dalje izvršenje funkcije i vraća generator objekat koji je napravljen u skladu sa “iterable protokolom” (iterable). Ključnu reč “yield” je dozvoljeneo koristiti samo u okviru generatora. Za nastavljanje izvršavanja generatorske funkcije se primenjuje iteratorova metoda “next()”.
Generatori
Generator je specifični tip iteratora, koji nastaje pozivanjem generatorske funkcije. Generatorska funkcija generiše iterable skup podataka, stoga možemo da koristimo next() metodu kao i “for..of” petlju za iteraciju. Razlike izmedju “običnog” iteratora i generatora su:
- kod generatora metoda može da primi parametar gen.next(value)
- postoji dodatna metoda throw()
Kao i kod iteratora, “next()” metoda vraća člana kolekcije “obmotanog” sa objektom koji ima dva svojstva: vrednost i done. Vrednost svojstva može biti dobijena direktno sa yield ili kao vraćena vrednost parametra generatora. Ključna reč “yield” uvek vraća neku vrednost čak iako je null.
“Generatori imaju ugradjen komunikacijski kanal sa yield”
1 2 3 4 5 6 7 |
function* channel () { var name = yield 'Zdravo, vaše ime je?' return 'Zdravo, ja sam ' + name } var gen = channel() console.log(gen.next().value) // Zdravo, vaše ime je? (trenutno yielded vrednost iz generatora) console.log(gen.next('Pera')) // Zdravo, ja sam Pera (ovde je prosledjena vrednost zamenila prethodnu yield vrednost) |
Primer
U ovome primeru je nakon izvršenja prvog reda, ključna reč “yield” pauzira dalje izvršenje funkcije. Za nastavak funkcije je koršćena metoda “next()”, nakon čega metoda vraća sledećeg člana kolekcije “obmotanog” (eng. wrapped”) sa objektom koji ima dva svojstva: value i done.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Generatorska funkcija: function* numbers () { yield 1; yield 2; return 3; } // Sledeći kod ne izvršava generatorsku funkciju *numbers(), nego kreira generator (specifični iterator) var nekiGenerator = numbers(); // Nastavljanje iteracije sa next() metodom: console.log(nekiGenerator.next()); // Pokreće generator čita prvi red i vraća: { done: false, value: 1 } console.log(nekiGenerator.next()); // Vraća: { done: false, value: 2 } console.log(nekiGenerator.next()); // Vraća: { done: true, value: 3 } |
Zahvaljujući osobini generatorske funkcije koja može da se pri izvršavanju pauzira a zatim nastavi izvršavanje, samo sa generataorima i generatorski funkcijama je moguće da zaustavimo izvršenje jedne funkcije, startujemo drugu koja nam vraća vrednost, a tu vrednost pošaljemo nazad kroz parametar u prvu funkciju i nastavimo izvršavanje.
Primer
1 2 3 4 5 6 7 8 9 10 11 12 |
function* logGenerator() { console.log("pocetak"); console.log(1, yield); console.log(2, yield); console.log(3, yield); } var gen = logGenerator(); gen.next(); // pocetak gen.next('Pera'); // 1 Pera gen.next('Mika'); // 2 Mika gen.next('Zika'); // 3 Zika |
Primer
Zahvaljujući mogućnosti da funkcija može da zaustavi svoje izvršenje možemo nakon toga pozvati drugu funkciju kao u ovome primeru:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
var x=1; function* generatorskaFunkcija (){ console.log("x: ", x); yield; // Pauziranje console.log("x: ", x); } function nekaFunkcija () { x++; } // Konstruisanje novog iteratora koji se koristi za upravljanje generatorom var nekiGenerator = generatorskaFunkcija(); // Pokretranje generatorske funkcije nekiGenerator.next(); // Vraća: x:1 // Pozivamo drugu funkciju u sred izvršenje generatorske funkcije nekaFunkcija(); // x=2 nekaFunkcija(); // x=3 // Nastavak izvršenja generatorske funkcije nekiGenerator.next(); // Vraća: x: 3 |