Iterator


V této části si ukážeme návrhový vzor Iterator. Tento návrhový vzor slouží pro procházení položek různých datových struktur.

Proč Iterator použít

Iterator můžeme chtít použít, pokud chceme procházet nějakou datovou strukturu. S pomocí Iteratoru můžeme postupně navštívit každou položku datové struktury.

Tvorba Iteratoru v JavaScriptu

JavaScript umožňuje Iterator vytvořit implementací metody [Symbol.iterator]. Jedná se o speciální metodu, pomocí které můžeme například umožnit procházet objekt pomocí for of cyklu. Tato metoda musí vrátit objekt obsahující metodu next, která se použije v každé iteraci for of cyklu. Metoda next musí vracet objekt s těmito dvěmi vlastnostmi:

  • value - hodnota, kterou iterator vrátí (pokud je vlastnost done true, tak může být tato vlastnost vynechána)
  • done - určuje, jestli je již iterator hotový (true/false)

Následující ukázka použití metody [Symbol.iterator] ukazuje.

class Iterator {
    constructor(pocet) {
        this.pocet = pocet;
    }

    // protože objekt implementuje metodu [Symbol.iterator], stává
    // se iterable, kterou můžeme použít třeba ve for of cyklu
    [Symbol.iterator]() {
        let cislo = 0;
        let pocet = this.pocet;
        let pocetIteraci = 0;
        // metoda vrací objekt s metodou next, která bude
        // volána pro každou iteraci
        return {
            next() {
                cislo += 10;
                return {
                    // hodnota, která se vrátí
                    value: cislo,
                    // určuje jestli iterator skončil
                    done: pocetIteraci++ === pocet
                }
            }
        }
    }
}

const iterator = new Iterator(10);

// procházení objektu iterator pomocí for of cyklu
for (let cislo of iterator)
    console.log(cislo);
// vypíše se:
// 10
// 20
// 30
// 40
// 50
// 60
// 70
// 80
// 90
// 100

Klíčové slovo yield

Pro pohodlnější iterování můžeme použít klíčové slovo yield. Klíčové slovo yield můžeme v JavaScriptu používat v generator funkcích, které vracejí Generator objekt. Generator funkci vytvoříme tak, že při deklaraci funkce zapíšeme za klíčové slovo function hvězdičku (*).

Pomocí klíčového slova yield můžeme v generator funkcích pozastavit provádění kódu a vrátit nějakou hodnotu. Následující ukázka ukazuje jak to funguje.

// deklarace generator funkce
function* generator() {
    yield 10;
    yield 20;
    yield 30;
    yield 40;
    yield 50;
}

// vytvoření Generator objektu
// - generator funkce vracejí Generator objekt
const gen = generator();

// po každém zavolání metody next objektu gen se kód pozastaví
// na klíčovém slovu yield a vrátí se objekt s hodnotou, kterou vrací
// - metoda next vrací objekt s vlastnostmi value a done
console.log(gen.next().value); // 10
console.log(gen.next().value); // 20
console.log(gen.next().value); // 30
console.log(gen.next().value); // 40
console.log(gen.next().value); // 50

Pokud v generator funkci použijeme klíčové slovo return, tak se generator ukončí. Následující ukázka to ukazuje.

function* generator() {
    yield 10;
    yield 20;
    yield 30;
    return 40;
    yield 50; // k tomuto řádku se již nedojde
}

const gen = generator();

console.log(gen.next()); // {value: 10, done: false}
console.log(gen.next()); // {value: 20, done: false}
console.log(gen.next()); // {value: 30, done: false}
console.log(gen.next()); // {value: 40, done: true}
console.log(gen.next()); // {value: undefined, done: true}

Při volání metody next můžeme předávat také argument, který se v generator funkci nahradí za klíčové slovo yield. Následující ukázka ukazuje generator funkci k vypisování textu do konzole.

function* logGenerator() {
    console.log("0");
    console.log("1", yield);
    console.log("2", yield);
    console.log("3", yield);
}

const gen = logGenerator();

gen.next(); // první zavolání funkce next dojde k prvnímu klíčovému slovu yield
gen.next("creational");
gen.next("structural");
gen.next("behavioral");

Jako generator funkci můžeme označit také metodu [Symbol.iterator] a v některých případech si tak usnadnit její implementaci. Následující ukázka ukazuje předělanou metodu [Symbol.iterator] třídy Iterator, která byla ukázána dříve. Určitě uznáte že teď vypadá mnohem lépe.

class Iterator {
    constructor(pocet) {
        this.pocet = pocet;
    }

    // implementace metody [Symbol.iterator]
    // s použitím klíčového slova yield
    *[Symbol.iterator]() {
        let cislo = 0;
        while (cislo < 100) {
            cislo += 10;
            yield cislo;
        }
    }
}

const iterator = new Iterator(10);

// procházení objektu iterator pomocí for of cyklu
for (let cislo of iterator)
    console.log(cislo);
// vypíše se:
// 10
// 20
// 30
// 40
// 50
// 60
// 70
// 80
// 90
// 100

Umožnění jiných způsobů iterování

Objekt může mít jen jednu metodu [Symbol.iterator]. Pokud ale chceme umožnit více způsobů iterování přes objekt, tak si můžeme vytvořit metody, které vrací nový objekt s vlastní metodou [Symbol.iterator], kterou můžeme použít k iteraci. Jak to můžeme udělat ukazuje následující ukázka.

class Iterator {
    constructor() {
        this.hodnoty = [5, 3, 4, 7, 2];
    }

    *[Symbol.iterator]() {
        for (let hodnota of this.hodnoty) {
            yield hodnota;
        }
    }

    // getter pro iterování pozpátku
    get pozpatku() {
        const hodnoty = this.hodnoty;

        // vrací se nový objekt s metodou [Symbol.iterator]
        return {
            *[Symbol.iterator]() {
                for (let i = hodnoty.length-1; i >= 0; i--) {
                    yield hodnoty[i];
                }
            }
        }
    }
}


const iterator = new Iterator();

// procházení objektu iterator
for (let hodnota of iterator)
    console.log(hodnota);
// hodnoty se vypíší v tomto pořadí: 5, 3, 4, 7, 2

// procházení objektu iterator pozpatku
for (let hodnota of iterator.pozpatku)
    console.log(hodnota);
// hodnoty se vypíší v tomto pořadí: 2, 7, 4, 3, 5

Příklad - Procházení Tree

Jako příklad použití Iteratoru si ukážeme procházení stromové datové struktury. Následující ukázka ukazuje Binary Search Tree o kterém si můžete přečíst na mých stránkách o algoritmech a datových strukturách v JS. Obsahuje metodu insert pro vložení nové položky a metodu [Symbol.iterator] pro Depth First procházení. Jak metoda insert a depth first procházení funguje se můžete dozvědět na mých stránkách. Následující ukázka jen předělává metodu DFSPreOrder, která je na mých stránkách popsána, aby se Tree dal procházet pomocí for of cyklu.

class Node {
    constructor(value) {
        this.value = value;
        this.left = null;
        this.right = null;
    }
}

class BinarySearchTree {
    constructor() {
        this.root = null;
    }

    insert(value) {
        const newNode = new Node(value);

        if (!this.root) {
            this.root = newNode;
            return this;
        }

        let current = this.root;

        while (true) {
            if (value === current.value) return undefined;
            if (value > current.value) {
                if (!current.right) {
                    current.right = newNode;
                    return this;
                }
                current = current.right;
            } else {
                if (!current.left) {
                    current.left = newNode;
                    return this;
                }
                current = current.left;
            }
        }
    }

    *[Symbol.iterator]() {
        const traverse = function*(node) {
            yield node.value;
            if (node.left) yield * traverse(node.left);
            if (node.right) yield * traverse(node.right);
        }

        yield * traverse(this.root);
    }
}

const tree = new BinarySearchTree();
tree.insert(10).insert(5).insert(16).insert(13).insert(6).insert(2);
tree.insert(17).insert(7).insert(3).insert(14).insert(11);

// procházení Tree pomocí for of cyklu
for (let hodnota of tree)
    console.log(hodnota);
// hodnoty se vypíší v tomto pořadí:
// 10, 5, 2, 3, 6, 7, 16, 13, 11, 14, 17