Hrvatski

Sveobuhvatan vodič za razumijevanje i implementaciju JavaScript Iterator protokola, koji vam omogućuje stvaranje prilagođenih iteratora za napredno rukovanje podacima.

Demistificiranje JavaScript Iterator protokola i prilagođenih iteratora

JavaScriptov Iterator protokol pruža standardizirani način za prolazak kroz strukture podataka. Razumijevanje ovog protokola omogućuje programerima da učinkovito rade s ugrađenim iterabilnim objektima poput nizova i stringova te da stvaraju vlastite prilagođene iterabilne objekte prilagođene specifičnim strukturama podataka i zahtjevima aplikacije. Ovaj vodič pruža sveobuhvatno istraživanje Iterator protokola i načina implementacije prilagođenih iteratora.

Što je Iterator protokol?

Iterator protokol definira kako se objekt može iterirati, tj. kako se njegovim elementima može pristupiti sekvencijalno. Sastoji se od dva dijela: Iterable protokola i Iterator protokola.

Iterable protokol

Objekt se smatra iterabilnim (Iterable) ako ima metodu s ključem Symbol.iterator. Ova metoda mora vratiti objekt koji je u skladu s Iterator protokolom.

U suštini, iterabilni objekt zna kako stvoriti iterator za sebe.

Iterator protokol

Iterator protokol definira kako dohvatiti vrijednosti iz niza. Objekt se smatra iteratorom ako ima next() metodu koja vraća objekt s dva svojstva:

Metoda next() je glavni radni dio Iterator protokola. Svaki poziv metode next() pomiče iterator i vraća sljedeću vrijednost u nizu. Kada su sve vrijednosti vraćene, next() vraća objekt s done postavljenim na true.

Ugrađeni iterabilni objekti

JavaScript pruža nekoliko ugrađenih struktura podataka koje su inherentno iterabilne. To uključuje:

Ovi iterabilni objekti mogu se izravno koristiti s for...of petljom, spread sintaksom (...) i drugim konstrukcijama koje se oslanjaju na Iterator protokol.

Primjer s nizovima:


const myArray = ["apple", "banana", "cherry"];

for (const item of myArray) {
  console.log(item); // Izlaz: apple, banana, cherry
}

Primjer sa stringovima:


const myString = "Hello";

for (const char of myString) {
  console.log(char); // Izlaz: H, e, l, l, o
}

for...of petlja

for...of petlja je moćna konstrukcija za iteriranje preko iterabilnih objekata. Automatski rukuje složenostima Iterator protokola, olakšavajući pristup vrijednostima u nizu.

Sintaksa for...of petlje je:


for (const element of iterable) {
  // Kod koji se izvršava za svaki element
}

for...of petlja dohvaća iterator iz iterabilnog objekta (koristeći Symbol.iterator) i opetovano poziva next() metodu iteratora dok done ne postane true. Za svaku iteraciju, varijabla element dobiva vrijednost svojstva value koju vraća next().

Stvaranje prilagođenih iteratora

Iako JavaScript pruža ugrađene iterabilne objekte, prava snaga Iterator protokola leži u mogućnosti definiranja prilagođenih iteratora za vlastite strukture podataka. To vam omogućuje kontrolu nad načinom na koji se vaši podaci prolaze i pristupaju.

Evo kako stvoriti prilagođeni iterator:

  1. Definirajte klasu ili objekt koji predstavlja vašu prilagođenu strukturu podataka.
  2. Implementirajte Symbol.iterator metodu na vašoj klasi ili objektu. Ova metoda treba vratiti objekt iteratora.
  3. Objekt iteratora mora imati next() metodu koja vraća objekt sa svojstvima value i done.

Primjer: Stvaranje iteratora za jednostavan raspon

Stvorimo klasu pod nazivom Range koja predstavlja raspon brojeva. Implementirat ćemo Iterator protokol kako bismo omogućili iteriranje preko brojeva u rasponu.


class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  [Symbol.iterator]() {
    let currentValue = this.start;
    const that = this; // Spremi 'this' za korištenje unutar objekta iteratora

    return {
      next() {
        if (currentValue <= that.end) {
          return {
            value: currentValue++,
            done: false,
          };
        } else {
          return {
            value: undefined,
            done: true,
          };
        }
      },
    };
  }
}

const myRange = new Range(1, 5);

for (const number of myRange) {
  console.log(number); // Izlaz: 1, 2, 3, 4, 5
}

Objašnjenje:

Primjer: Stvaranje iteratora za povezanu listu

Razmotrimo još jedan primjer: stvaranje iteratora za strukturu podataka povezane liste. Povezana lista je niz čvorova, gdje svaki čvor sadrži vrijednost i referencu (pokazivač) na sljedeći čvor u listi. Posljednji čvor u listi ima referencu na null (ili undefined).


class LinkedListNode {
    constructor(value, next = null) {
        this.value = value;
        this.next = next;
    }
}

class LinkedList {
    constructor() {
        this.head = null;
    }

    append(value) {
        const newNode = new LinkedListNode(value);
        if (!this.head) {
            this.head = newNode;
            return;
        }

        let current = this.head;
        while (current.next) {
            current = current.next;
        }
        current.next = newNode;
    }

    [Symbol.iterator]() {
        let current = this.head;

        return {
            next() {
                if (current) {
                    const value = current.value;
                    current = current.next;
                    return {
                        value: value,
                        done: false
                    };
                } else {
                    return {
                        value: undefined,
                        done: true
                    };
                }
            }
        };
    }
}

// Primjer korištenja:
const myList = new LinkedList();
myList.append("London");
myList.append("Paris");
myList.append("Tokyo");

for (const city of myList) {
    console.log(city); // Izlaz: London, Paris, Tokyo
}

Objašnjenje:

Generatorske funkcije

Generatorske funkcije pružaju sažetiji i elegantniji način za stvaranje iteratora. Koriste ključnu riječ yield za proizvodnju vrijednosti na zahtjev.

Generatorska funkcija definira se pomoću sintakse function*.

Primjer: Stvaranje iteratora pomoću generatorske funkcije

Prepišimo Range iterator koristeći generatorsku funkciju:


class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  *[Symbol.iterator]() {
    for (let i = this.start; i <= this.end; i++) {
      yield i;
    }
  }
}

const myRange = new Range(1, 5);

for (const number of myRange) {
  console.log(number); // Izlaz: 1, 2, 3, 4, 5
}

Objašnjenje:

Generatorske funkcije pojednostavljuju stvaranje iteratora automatskim rukovanjem next() metodom i done zastavicom.

Primjer: Generator Fibonaccijevog niza

Još jedan sjajan primjer korištenja generatorskih funkcija je generiranje Fibonaccijevog niza:


function* fibonacciSequence() {
  let a = 0;
  let b = 1;

  while (true) {
    yield a;
    [a, b] = [b, a + b]; // Destrukturirajuća dodjela za istovremeno ažuriranje
  }
}

const fibonacci = fibonacciSequence();

for (let i = 0; i < 10; i++) {
  console.log(fibonacci.next().value); // Izlaz: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
}

Objašnjenje:

Prednosti korištenja Iterator protokola

Napredne tehnike s iteratorima

Kombiniranje iteratora

Možete kombinirati više iteratora u jedan jedini iterator. To je korisno kada trebate obraditi podatke iz više izvora na jedinstven način.


function* combineIterators(...iterables) {
  for (const iterable of iterables) {
    for (const item of iterable) {
      yield item;
    }
  }
}

const array1 = [1, 2, 3];
const array2 = ["a", "b", "c"];
const string1 = "XYZ";

const combined = combineIterators(array1, array2, string1);

for (const value of combined) {
  console.log(value); // Izlaz: 1, 2, 3, a, b, c, X, Y, Z
}

U ovom primjeru, funkcija `combineIterators` prima bilo koji broj iterabilnih objekata kao argumente. Iterira preko svakog iterabilnog objekta i vraća (yields) svaku stavku. Rezultat je jedan iterator koji proizvodi sve vrijednosti iz svih ulaznih iterabilnih objekata.

Filtriranje i transformiranje iteratora

Također možete stvoriti iteratore koji filtriraju ili transformiraju vrijednosti proizvedene od strane drugog iteratora. To vam omogućuje obradu podataka u cjevovodu (pipeline), primjenjujući različite operacije na svaku vrijednost kako se generira.


function* filterIterator(iterable, predicate) {
  for (const item of iterable) {
    if (predicate(item)) {
      yield item;
    }
  }
}

function* mapIterator(iterable, transform) {
  for (const item of iterable) {
    yield transform(item);
    }
}

const numbers = [1, 2, 3, 4, 5, 6];

const evenNumbers = filterIterator(numbers, (x) => x % 2 === 0);
const squaredEvenNumbers = mapIterator(evenNumbers, (x) => x * x);

for (const value of squaredEvenNumbers) {
    console.log(value); // Izlaz: 4, 16, 36
}

Ovdje, `filterIterator` prima iterabilni objekt i predikatnu funkciju. Vraća (yields) samo stavke za koje predikat vrati `true`. `mapIterator` prima iterabilni objekt i funkciju za transformaciju. Vraća (yields) rezultat primjene funkcije transformacije na svaku stavku.

Primjene u stvarnom svijetu

Iterator protokol se široko koristi u JavaScript bibliotekama i okvirima, te je vrijedan u raznim primjenama u stvarnom svijetu, posebno kada se radi s velikim skupovima podataka ili asinkronim operacijama.

Najbolje prakse

Zaključak

JavaScript Iterator protokol pruža moćan i fleksibilan način za prolazak kroz strukture podataka. Razumijevanjem Iterable i Iterator protokola te korištenjem generatorskih funkcija, možete stvoriti prilagođene iteratore prilagođene vašim specifičnim potrebama. To vam omogućuje učinkovit rad s podacima, poboljšanje čitljivosti koda i poboljšanje performansi vaših aplikacija. Ovladavanje iteratorima otključava dublje razumijevanje JavaScriptovih mogućnosti i omogućuje vam pisanje elegantnijeg i učinkovitijeg koda.