Iteratory i generatory

Przemieszczenie się po różnych strukturach danych jest bardzo powszechną czynnością w programowaniu.

Przez wiele lat mogliśmy spacerować po tablicach, stringach czy obiektach za pomocą klasycznych pętli for:


const tab = ["a", "b", "c"];
for (let i=0; i<tab.length; i++) {
    console.log(tab[i]);
}


const txt = "abc";
for (let i=0; i<txt.length; i++) {
    console.log(txt[i]);
}


const ob = {
    name : "Karol",
    age: 10
}
for (const key in ob) {
    console.log(ob[key]);
}

W ECMAScript 2015 wprowadzono nowy mechanizm poruszania się po strukturach danych zwany iteracją, który bezpośrednio związany jest z pętlą for of. Dzięki niemu możemy iterować praktycznie po każdej strukturze danych.

Iteracja

Domyślnie typami po których możemy iterować są:

Każdy z tych typów danych ma zaimplementowaną metodę, która zwraca tak zwany Iterator.

Iterator to obiekt, który potrafi odwołać się do kolejnych elementów z danej struktury, a równocześnie wywoływany w sekwencji potrafi zapamiętać swoją bieżącą pozycję. Taki obiekt zawiera metodę next(), która służy do zwracania kolejnego elementu w kolekcji.

W Javascript funkcje zwracające iteratory zaimplementowane są pod kluczami Symbol.iterator:


const tab = ["Ala", "Bala", "Cala"];
const iterator = tab[Symbol.iterator]();

console.log(iterator.next()); //{value: "Ala", done: false}
console.log(iterator.next()); //{value: "Bala", done: false}
console.log(iterator.next()); //{value: "Cala", done: false}
console.log(iterator.next()); //{value: undefined, done: true}

Jak widzisz, za każdym wywołaniem metody next() iteratora, zwracany jest w odpowiedzi obiekt, który ma 2 klucze: value czyli wartość kolejnego elementu, oraz done, które oznacza, czy doszliśmy do końca struktury danych. Jeżeli dane wywołanie nie znajduje już kolejnego elementu, done wyniesie true.

Powyżej wymienione typy danych mają domyślnie zaimplementowane funkcje, które znajdują się pod kluczem Symbol.iterator:


const txt = "Ala ma kota";
const tab = ["Ala", "Bala", "Cala"];
const set = new Set();
const map = new Map();
const btn = document.querySelectorAll("button");

console.log(txt[Symbol.iterator]); //function Symbol.iterator
console.log(tab[Symbol.iterator]); //function Symbol.iterator
console.log(set[Symbol.iterator]); //function Symbol.iterator
console.log(map[Symbol.iterator]); //function Symbol.iterator
console.log(btn[Symbol.iterator]); //function Symbol.iterator

Dzięki czemu możemy po nich iterować:


const tab = ["Ala", "Bala", "Cala"];

for (const el of tab) {
    console.log(el); //"Ala", "Bala", "Cala"...
}

const iter = tab[Symbol.iterator]();
iter.next(); //{value: "Ala", done: false}
iter.next(); //{value: "Bala", done: false}
iter.next(); //{value: "Cala", done: false}
iter.next(); //{value: undefined, done: done}

Ale też możemy dane struktury rozbijać za pomocą spread syntax:


console.log(...tab); //"Ala", "Bala", "Cala"
console.log(...txt); //"A", "l", "a"...

Dla innych obiektów nie mamy takich funkcji, co powoduje, że ani iteracja ani rozbijanie się nie powiedzie:


const ob = {
    names: ["Ala", "Bala", "Cala"]
}

for (const el of ob) { //error: ob is not iterable
    console.log(el);
}
console.log(...ob); //error: Found non-callable @@iterator

W każdym momencie możemy jednak takie funkcje dorobić.

Funkcja taka powinna zwracać obiekt iteratora z metodą next(). Przykładowa implementacja takiej metody ma postać:


const ob = {
    names: ["Ala", "Bala", "Cala"],

    [Symbol.iterator] : function() {
        let index = 0;
        const names = this.names;

        return {
            next() {
                if (index >= names.length) {
                    index = 0;
                    return { value: undefined, done : true }
                } else {
                    return { value: names[index++], done : false }
                }
            }
        }
    }
}

const iterator = ob[Symbol.iterator]();
iterator.next(); //{value: "Ala", done: false}
iterator.next(); //{value: "Bala", done: false}
iterator.next(); //{value: "Cala", done: false}
iterator.next(); //{value: undefined, done: true}

for (const el of ob) {
    console.log(el); //"Ala", "Bala", "Cala"...
}

Generatory

Jak widzisz powyżej, własnoręczna implementacja iteratorów nie zawsze jest najłatwiejszą sprawą, ponieważ wymaga od nas utrzymywania ich wewnętrznego stanu.

Aby usprawnić ten mechanizm w ECMAScript 2015 poza iteratorami wprowadzono też generatory, czyli funkcje, które są w stanie zapamiętać stan pomiędzy kolejnymi wywołaniami.

Funkcja staje się generatorem, gdy zawiera przynajmniej jedno wystąpienie słowa yield, oraz przy jej deklaracji pojawia się znak *.

Generator automatycznie zwraca metodę next(), która przy każdorazowym użyciu będzie zwracać kolejne wystąpienia yield:


function* generator() {
    yield "kot";
    yield "pies";
    yield "świnka";
}

const gen = generator();
gen.next(); //{value: "kot", done: false}
gen.next(); //{value: "pies", done: false}
gen.next(); //{value: "świnka", done: false}
gen.next(); //{value: undefined, done: true}

function* tabLoop() {
    const tab = ["ala", "bala", "cala"];

    for (const el of tab) {
        yield el;
    }
}

const gen = tabLoop();
gen.next(); //{value: "ala", done: false}
gen.next(); //{value: "bala", done: false}
gen.next(); //{value: "cala", done: false}
gen.next(); //{value: undefined, done: true}

Podobny generator możemy zaimplementować w poprzednio stworzonym obiekcie:


const ob = {
    names: ["Ala", "Bala", "Cala"],

    *[Symbol.iterator]() {
        for (const el of this.names) {
            yield el;
        }
    }
}

const iterator = ob[Symbol.iterator]();
iterator.next(); //{value: "Ala", done: false}
iterator.next(); //{value: "Bala", done: false}
iterator.next(); //{value: "Cala", done: false}
iterator.next(); //{value: undefined, done: true}

for (const el of ob) {
    console.log(el); //"Ala", "Bala", "Cala"...
}

Funkcje zwracające iteratory

Javascript udostępnia nam dodatkowo kilka funkcji, dzięki którym możemy w przyjemniejszy sposób iterować po wartościach czy kluczach danego obiektu.

Są to kolejno:

Object.keys(ob) Zwraca tablicę kluczy danego obiektu
Object.values(ob) Zwraca tablicę wartości danego obiektu
Object.entries(ob) Zwraca tablicę par [klucz-wartość]

const car = {
    brand : "BMW",
    color : "red",
    speed : 150
}

for (const key of Object.keys(car)) {
    console.log(key); //brand, color, speed
}

for (const val of Object.values(car)) {
    console.log(val); //"BMW", "red", 150
}

for (const [key, val] of Object.entries(car)) {
    console.log(key, val); //[brand, "BMW"], [color, "red"], [speed, 150]
}

//...
//pętli for of bezpośrednio po powyższym obiekcie nie zrobimy, bo nie zaimplementowaliśmy
//mu funkcji *[Symbol.iterator]()

W pętli po entries zastosowaliśmy destrukturyzaję by wyciągnąć pod zmienne key i val.

Dla wspomnianych powyżej tablic, kolekcji DOM, Stringów, Map czy Setów omawiane funkcje także zadziałają:


const tab = ["Ala", "Bala", "Cala"];

for (const key of Object.keys(tab)) {
    console.log(key); //"0", "1"...
}

for (const val of Object.values(tab)) {
    console.log(val); //"Ala", "Bala"...
}

for (const [key, val] of Object.entries(tab)) {
    console.log(key, val); //["0" ,"Ala"], ["1", "Bala"]...
}

//dla tablic możemy zrobić pętlę for of bezpośrednio po tablicy
//bo domyślnie mają one swój iterator
for (const el of tab) {
    console.log(el); //"Ala", "Bala"...
}

const txt = "Kotek";

for (const key of Object.keys(txt)) {
    console.log(key); //"0", "1"...
}

for (const val of Object.values(txt)) {
    console.log(val); //"K", "o", "t"...
}

for (const [key, val] of Object.entries(txt)) {
    console.log(key, val); //["0" ,"K"], ["1", "o"], ["2", "t"]...
}

for (const el of txt) {
    console.log(el); //"K", "o", "t"...
}

Trening czyni mistrza

Jeżeli chcesz sobie potrenować zdobytą wiedzę, zadania znajdują się w repozytorium pod adresem: https://github.com/kartofelek007/zadania

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem.
Aha - i ta strona korzysta z ciasteczek.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.