Kurs JavaScript i TypeScript · Moduł 4: Programowanie asynchroniczne

Iteratory i Generatory

6 min czytania
W tej lekcji6

Genom dinozaura to miliardy par zasad. Gdyby laboratorium Parku Jurajskiego wczytywało całą sekwencję naraz, komputery padłyby, zanim ktokolwiek przeczytałby pierwszy gen. Naukowcy analizują więc DNA fragment po fragmencie, na żądanie. Iteratory i generatory w JavaScript działają tak samo: pozwalają przetwarzać dane element po elemencie, leniwie i oszczędnie. "To taśma produkcyjna w laboratorium" - mówi Dr. Rex. "Dostajesz kolejną próbkę dokładnie wtedy, gdy jej potrzebujesz."

Protokół iterowalności

Każdy obiekt, który ma metodę [Symbol.iterator](), jest iterowalny i może trafić do pętli for...of. Tablice i stringi mają ją od urodzenia:

1// Tablice, stringi, Map, Set - są iterowalne z natury
2for (const char of 'ATCG') {
3  console.log(char); // A, T, C, G
4}
5
6for (const item of [1, 2, 3]) {
7  console.log(item); // 1, 2, 3
8}

Pod spodem for...of wywołuje [Symbol.iterator]() i dostaje iterator, czyli obiekt z metodą next(). Możesz ją wywołać ręcznie:

1const it = ['T-Rex', 'Raptor'][Symbol.iterator]();
2console.log(it.next()); // { value: 'T-Rex', done: false }
3console.log(it.next()); // { value: 'Raptor', done: false }
4console.log(it.next()); // { value: undefined, done: true }

Każde wywołanie zwraca obiekt z polami value i done. Pętla kończy się, gdy done ma wartość true, a samo value z tego ostatniego wyniku jest już pomijane.

Tworzenie własnego iteratora

Zwykły obiekt nie jest iterowalny. Żeby wybieg dało się obejść pętlą, dopisujemy mu [Symbol.iterator](), który zwraca obiekt z next():

1const dinoEnclosure = {
2  dinosaurs: ['Rex', 'Blue', 'Delta', 'Echo'],
3
4  [Symbol.iterator]() {
5    let index = 0;
6    const dinos = this.dinosaurs;
7
8    return {
9      next() {
10        if (index < dinos.length) {
11          return { value: dinos[index++], done: false };
12        }
13        return { value: undefined, done: true };
14      }
15    };
16  }
17};
18
19// Teraz możemy użyć for...of!
20for (const dino of dinoEnclosure) {
21  console.log('Dinozaur:', dino);
22}
23
24// Oraz spread operator
25const allDinos = [...dinoEnclosure];
26console.log(allDinos); // ['Rex', 'Blue', 'Delta', 'Echo']

Licznik index żyje w domknięciu, więc każda nowa pętla zaczyna od zera. Spread działa od razu, bo korzysta z tego samego protokołu co for...of.

for...of vs for...in

Te dwie pętle mają podobne nazwy, ale zupełnie inne zadania. Porównaj je na obiekcie i na tablicy:

1const park = { name: 'Jurassic', size: 'large', open: false };
2const species = ['T-Rex', 'Velociraptor', 'Triceratops'];
3
4// for...in - iteruje po KLUCZACH (obiektów i tablic)
5for (const key in park) {
6  console.log(key); // 'name', 'size', 'open'
7}
8
9for (const index in species) {
10  console.log(index); // '0', '1', '2' (stringi!)
11}
12
13// for...of - iteruje po WARTOŚCIACH (iterowalnych)
14for (const animal of species) {
15  console.log(animal); // 'T-Rex', 'Velociraptor', 'Triceratops'
16}
17
18// for...of NIE działa na zwykłych obiektach!
19// for (const val of park) {} // TypeError!
20// Ale działa na Object.entries():
21for (const [key, val] of Object.entries(park)) {
22  console.log(key, val);
23}

for...in zwraca klucze jako stringi i odwiedza też właściwości odziedziczone z prototypu. Moja rada: po tablicach chodź for...of, a po obiektach for...of z Object.entries().

Generatory (function*)

Pisanie next() ręcznie jest żmudne. Generator robi tę księgowość za ciebie: to funkcja oznaczona gwiazdką function*, która potrafi wstrzymać działanie na yield i wznowić je później:

1function* dinoGenerator() {
2  console.log('Generuję pierwszego dinozaura...');
3  yield 'T-Rex';
4
5  console.log('Generuję drugiego...');
6  yield 'Velociraptor';
7
8  console.log('Generuję trzeciego...');
9  yield 'Triceratops';
10
11  console.log('Koniec generacji!');
12}
13
14const gen = dinoGenerator();
15
16console.log(gen.next()); // { value: 'T-Rex', done: false }
17console.log(gen.next()); // { value: 'Velociraptor', done: false }
18console.log(gen.next()); // { value: 'Triceratops', done: false }
19console.log(gen.next()); // { value: undefined, done: true }

Samo wywołanie dinoGenerator() niczego nie wypisuje, tylko zwraca obiekt generatora. Kod rusza dopiero przy pierwszym next() i biegnie do najbliższego yield, więc każdy komunikat pojawia się tuż przed swoim wynikiem. Generator może też zakończyć się przez return:

1function* dinos() {
2  yield 'T-Rex';
3  yield 'Raptor';
4  return 'done';
5}
6
7const herd = dinos();
8console.log(herd.next()); // { value: 'T-Rex', done: false }
9console.log(herd.next()); // { value: 'Raptor', done: false }
10console.log(herd.next()); // { value: 'done', done: true }
11console.log([...dinos()]); // ['T-Rex', 'Raptor'] - bez 'done'

Wartość z return przychodzi razem z done: true, dlatego for...of i spread ją pomijają.

Generator nieskończony

Generator może nigdy się nie kończyć, bo kolejną wartość liczy dopiero na żądanie:

1function* idGenerator(prefix) {
2  let id = 1;
3  while (true) {
4    yield prefix + '-' + String(id).padStart(3, '0');
5    id++;
6  }
7}
8
9const dinoIds = idGenerator('DINO');
10console.log(dinoIds.next().value); // 'DINO-001'
11console.log(dinoIds.next().value); // 'DINO-002'
12console.log(dinoIds.next().value); // 'DINO-003'
13// Nigdy się nie kończy - generuje na żądanie!

Pętla while (true) nie zawiesza programu, bo wykonanie staje przy każdym yield. Nigdy jednak nie rozpakowuj takiego generatora spreadem: [...dinoIds] próbowałby zebrać nieskończenie wiele wartości.

yield* - delegacja do innego generatora

yield* przekazuje po kolei wszystkie wartości innego generatora lub iterowalnego obiektu, więc małe generatory można składać w większe:

1function* carnivores() {
2  yield 'T-Rex';
3  yield 'Velociraptor';
4}
5
6function* herbivores() {
7  yield 'Triceratops';
8  yield 'Brachiosaurus';
9}
10
11function* allDinosaurs() {
12  yield* carnivores();
13  yield* herbivores();
14}
15
16for (const dino of allDinosaurs()) {
17  console.log(dino);
18}
19// T-Rex, Velociraptor, Triceratops, Brachiosaurus

Skoro yield* działa też na tablicach, własny iterator wybiegu skrócisz do jednej linii: *[Symbol.iterator]() { yield* this.dinosaurs; }. Tak polecam go pisać.

Async Generatory

Dane z serwera przychodzą stronami, a każda wymaga await. Async generator łączy async z function*, a jego wyniki odbiera pętla for await...of:

1async function* fetchDinoPages(apiUrl) {
2  let page = 1;
3  let hasMore = true;
4
5  while (hasMore) {
6    const response = await fetch(apiUrl + '?page=' + page);
7    const data = await response.json();
8
9    yield data.results;
10
11    hasMore = data.hasNextPage;
12    page++;
13  }
14}
15
16// Użycie z for await...of
17async function loadAllDinos() {
18  for await (const batch of fetchDinoPages('/api/dinosaurs')) {
19    console.log('Załadowano grupę:', batch.length, 'dinozaurów');
20  }
21}

Kolejna strona ładuje się dopiero wtedy, gdy pętla poprosi o następną porcję. Jeśli nikt nie przetworzy wyników, żadne dodatkowe zapytanie nie wyjdzie do serwera.

Praktyczne zastosowania

Pierwszy klasyk to leniwe obliczenia: ciąg Fibonacciego jest nieskończony, a my bierzemy z niego tylko tyle, ile potrzeba:

1// 1. Lazy evaluation - przetwarzanie na żądanie
2function* fibonacci() {
3  let a = 0, b = 1;
4  while (true) {
5    yield a;
6    [a, b] = [b, a + b];
7  }
8}
9
10// Weź tylko 10 pierwszych
11const fib = fibonacci();
12const first10 = [];
13for (let i = 0; i < 10; i++) {
14  first10.push(fib.next().value);
15}
16console.log(first10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Linia [a, b] = [b, a + b] to zamiana wartości przez destrukturyzację, którą znasz z modułu 2. Drugi klasyk to paginacja, czyli podawanie danych porcjami:

1// 2. Paginacja - ładuj strony na żądanie
2function* paginate(items, pageSize) {
3  for (let i = 0; i < items.length; i += pageSize) {
4    yield items.slice(i, i + pageSize);
5  }
6}
7
8const allSpecies = ['A','B','C','D','E','F','G','H'];
9const pages = paginate(allSpecies, 3);
10console.log(pages.next().value); // ['A', 'B', 'C']
11console.log(pages.next().value); // ['D', 'E', 'F']
12console.log(pages.next().value); // ['G', 'H']

Ostatnia strona ma tylko dwa elementy, bo slice zatrzymuje się na końcu tablicy. Ten sam wzorzec pętli z krokiem i yield wystarczy, by napisać własny range(start, end, step).

Praktyka

W piaskownicy czekają własny iterator wybiegu i generatory do przećwiczenia. Wywołuj next() ręcznie i przewiduj, kiedy pojawi się done: true.

Kod do tej lekcji: index.js
1// === ITERATORY I GENERATORY ===
2
3// 1. Własny iterator
4const enclosure = {
5  dinosaurs: ['Rex', 'Blue', 'Delta', 'Echo'],
6  [Symbol.iterator]() {
7    let i = 0;
8    const dinos = this.dinosaurs;
9    return {
10      next() {
11        return i < dinos.length
12          ? { value: dinos[i++], done: false }
13          : { done: true };
14      }
15    };
16  }
17};
18
19console.log('=== Iterator ===');
20for (const dino of enclosure) {
21  console.log('Dino:', dino);
22}
23console.log('Spread:', [...enclosure]);
24
25// 2. for...of vs for...in
26console.log('\n=== for...of vs for...in ===');
27const species = ['T-Rex', 'Raptor', 'Triceratops'];
28console.log('for...in (klucze):');
29for (const i in species) console.log(' ', i, typeof i);
30console.log('for...of (wartości):');
31for (const s of species) console.log(' ', s);
32
33// 3. Generator
34function* dinoGen() {
35  yield 'T-Rex';
36  yield 'Velociraptor';
37  yield 'Triceratops';
38}
39console.log('\n=== Generator ===');
40const gen = dinoGen();
41console.log(gen.next()); // { value: 'T-Rex', done: false }
42console.log(gen.next()); // { value: 'Velociraptor', done: false }
43console.log(gen.next()); // { value: 'Triceratops', done: false }
44console.log(gen.next()); // { done: true }
45
46// 4. Nieskończony generator
47function* idGen(prefix) {
48  let id = 1;
49  while (true) {
50    yield prefix + '-' + String(id++).padStart(3, '0');
51  }
52}
53console.log('\n=== ID Generator ===');
54const ids = idGen('DINO');
55console.log(ids.next().value); // DINO-001
56console.log(ids.next().value); // DINO-002
57console.log(ids.next().value); // DINO-003
58
59// 5. yield* - delegacja
60function* carnivores() { yield 'T-Rex'; yield 'Raptor'; }
61function* herbivores() { yield 'Triceratops'; yield 'Brachio'; }
62function* allDinos() { yield* carnivores(); yield* herbivores(); }
63console.log('\n=== yield* ===');
64console.log([...allDinos()]);
65
66// 6. Paginacja z generatorem
67function* paginate(items, size) {
68  for (let i = 0; i < items.length; i += size) {
69    yield items.slice(i, i + size);
70  }
71}
72console.log('\n=== Paginacja ===');
73const pages = paginate(['A','B','C','D','E','F','G'], 3);
74console.log('Page 1:', pages.next().value);
75console.log('Page 2:', pages.next().value);
76console.log('Page 3:', pages.next().value);

Zapamiętaj: generator to taśma w laboratorium, która podaje kolejną próbkę dopiero wtedy, gdy wyciągniesz po nią rękę.

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Jaką metodę musi implementować obiekt, aby być iterowalny (używalny w for...of)?

  2. 2. Jak oznaczamy funkcję jako generator w JavaScript?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Klikanie w kolejności

    Ułóż elementy tworzenia async generatora:

  • Edytor kodu

    Zaimplementuj [Symbol.iterator], generator ID i generator paginacji

  • Układanie w pionie

    Ułóż kroki działania iteratora od pierwszego wywołania next() do zakończenia:

  • Klikanie w kolejności

    Ułóż elementy, aby stworzyć prosty generator:

  • Edytor kodu

    Zaimplementuj Fibonacci, yield* do łączenia generatorów i generator range(start, end, step)

  • Układanie w pionie

    Ułóż elementy implementacji protokołu iteratora:

  • Klikanie w kolejności

    Ułóż elementy generatora z wielokrotnym yield:

Przydatne artykuły