Kurs JavaScript i TypeScript · Moduł 5: Zaawansowany JavaScript
Iteratory i protokół iterowalny
W tej lekcji8
W laboratorium genetycznym Parku Jurajskiego, Dr. Henry Wu przegląda ogromne zbiory danych DNA dinozaurów. "Potrzebujemy sposobu, aby przetwarzać sekwencje genetyczne element po elemencie, bez ładowania całej bazy do pamięci," mówi. Pętla for...of radzi sobie z tablicą od ręki, ale przepuść przez nią zwykły obiekt parku z sektorami, a dostaniesz TypeError z informacją, że obiekt nie jest iterowalny. Na szczęście JavaScript oferuje iteratory i protokół iterowalny - mechanizmy, które pozwalają kontrolować sposób przeglądania kolekcji danych.
Czym jest iterator?
Iterator to obiekt, który wie, jak uzyskać dostęp do elementów kolekcji po jednym naraz, śledząc swoją aktualną pozycję. Iterator musi implementować metodę next(), która zwraca obiekt z dwoma właściwościami:
value- aktualna wartośćdone-truejeśli nie ma więcej elementów,falsew przeciwnym przypadku
1// Prosty iterator - ręcznie stworzony
2function createDinoIterator(dinosaurs) {
3 let index = 0;
4
5 return {
6 next() {
7 if (index < dinosaurs.length) {
8 return { value: dinosaurs[index++], done: false };
9 }
10 return { value: undefined, done: true };
11 }
12 };
13}
14
15const raptorIterator = createDinoIterator(["Blue", "Charlie", "Delta", "Echo"]);
16
17console.log(raptorIterator.next()); // { value: "Blue", done: false }
18console.log(raptorIterator.next()); // { value: "Charlie", done: false }
19console.log(raptorIterator.next()); // { value: "Delta", done: false }
20console.log(raptorIterator.next()); // { value: "Echo", done: false }
21console.log(raptorIterator.next()); // { value: undefined, done: true }Zmienna index żyje w domknięciu i pamięta pozycję między wywołaniami next(). Po ostatnim raptorze iterator zgłasza done: true i odpowiada tak już zawsze - nie zaczyna od nowa. Przekazana tablica nie zmieniła się ani trochę.
Protokół iterowalny (Symbol.iterator)
Aby obiekt mógł być użyty w pętli for...of, musi implementować protokół iterowalny - czyli posiadać metodę [Symbol.iterator](), która zwraca iterator. Wiele wbudowanych typów już to implementuje: Array, String, Map, Set.
1// Tablice są iterowalne
2const species = ["T-Rex", "Velociraptor", "Triceratops"];
3
4for (const dino of species) {
5 console.log(dino);
6}
7// T-Rex
8// Velociraptor
9// Triceratops
10
11// Stringi też są iterowalne
12for (const char of "DINO") {
13 console.log(char); // D, I, N, O
14}
15
16// Możemy ręcznie pobrać iterator z tablicy
17const iterator = species[Symbol.iterator]();
18console.log(iterator.next()); // { value: "T-Rex", done: false }
19console.log(iterator.next()); // { value: "Velociraptor", done: false }Pętla for...of robi za kulisami to, co widać w ostatnich liniach: pobiera iterator przez Symbol.iterator i woła next(), aż zobaczy done: true. Symbol.iterator to wbudowany symbol, czyli unikalny klucz, którego nie pomylisz ze zwykłą nazwą właściwości. Literał obiektu takiej metody nie ma i stąd błąd z wprowadzenia.
Tworzenie własnego obiektu iterowalnego
Możemy sprawić, by dowolny obiekt był iterowalny - wystarczy zdefiniować metodę [Symbol.iterator](). Nasz park przejdzie przez wszystkie sektory, oddając po jednym dinozaurze:
1// Park Jurajski jako obiekt iterowalny
2const jurassicPark = {
3 name: "Jurassic Park",
4 sectors: [
5 { id: "A", dinosaurs: ["Rexy", "Blue"] },
6 { id: "B", dinosaurs: ["Spike", "Trike"] },
7 { id: "C", dinosaurs: ["Pteranodon", "Mosasaurus"] }
8 ],
9
10 // Implementacja protokołu iterowalnego
11 [Symbol.iterator]() {
12 let sectorIndex = 0;
13 let dinoIndex = 0;
14 const sectors = this.sectors;
15
16 return {
17 next() {
18 // Przechodzimy przez wszystkie dinozaury we wszystkich sektorach
19 while (sectorIndex < sectors.length) {
20 const sector = sectors[sectorIndex];
21 if (dinoIndex < sector.dinosaurs.length) {
22 const value = {
23 sector: sector.id,
24 dinosaur: sector.dinosaurs[dinoIndex]
25 };
26 dinoIndex++;
27 return { value, done: false };
28 }
29 sectorIndex++;
30 dinoIndex = 0;
31 }
32 return { value: undefined, done: true };
33 }
34 };
35 }
36};
37
38// Teraz możemy iterować po parku!
39for (const entry of jurassicPark) {
40 console.log(`Sektor ${entry.sector}: ${entry.dinosaur}`);
41}
42// Sektor A: Rexy
43// Sektor A: Blue
44// Sektor B: Spike
45// Sektor B: Trike
46// Sektor C: Pteranodon
47// Sektor C: Mosasaurus
48
49// Działa też spread operator i destrukturyzacja
50const allDinos = [...jurassicPark];
51console.log(allDinos.length); // 6Nawiasy kwadratowe przy nazwie metody oznaczają, że kluczem jest symbol, a nie tekst. Pętla while w next() przeskakuje do kolejnego sektora, gdy w bieżącym zabraknie dinozaurów, więc na zewnątrz widać jedną płaską listę. Dane w sectors zostają nietknięte - iterator tylko je czyta.
Generatory jako iteratory
Ręczne pisanie next() jest żmudne. Generatory to specjalne funkcje oznaczone gwiazdką (function*), które upraszczają tworzenie iteratorów. Używają słowa kluczowego yield do "wyprodukowania" kolejnych wartości:
1// Generator sekwencji DNA
2function* dnaSequenceGenerator(sequence) {
3 for (const nucleotide of sequence) {
4 yield nucleotide;
5 }
6}
7
8const dna = dnaSequenceGenerator("ATCGATCG");
9console.log(dna.next()); // { value: "A", done: false }
10console.log(dna.next()); // { value: "T", done: false }
11console.log(dna.next()); // { value: "C", done: false }
12
13// Generatory są iterowalne - działają z for...of
14for (const nucleotide of dnaSequenceGenerator("GCTA")) {
15 console.log(nucleotide); // G, C, T, A
16}Wywołanie generatora nie uruchamia jeszcze jego ciała, tylko zwraca obiekt generatora. Każde next() wykonuje kod do najbliższego yield i tam go wstrzymuje, pamiętając stan. Gwiazdka stoi przy słowie function, nie przy nazwie - zapis function myGen*() jest błędem składni.
Generator z logiką
Generator może produkować wartości bez końca, bo liczy każdą dopiero wtedy, gdy ktoś o nią poprosi. Tak działa maszyna nadająca identyfikatory:
1// Generator identyfikatorów dinozaurów
2function* dinoIdGenerator(prefix, startFrom = 1) {
3 let id = startFrom;
4 while (true) {
5 yield `${prefix}-${String(id).padStart(3, "0")}`;
6 id++;
7 }
8}
9
10const raptorIds = dinoIdGenerator("RAPTOR");
11console.log(raptorIds.next().value); // "RAPTOR-001"
12console.log(raptorIds.next().value); // "RAPTOR-002"
13console.log(raptorIds.next().value); // "RAPTOR-003"
14
15// Generator nieskończony - ale pobieramy tylko tyle ile potrzebujemy
16const trexIds = dinoIdGenerator("TREX", 100);
17const firstFive = [];
18for (let i = 0; i < 5; i++) {
19 firstFive.push(trexIds.next().value);
20}
21console.log(firstFive);
22// ["TREX-100", "TREX-101", "TREX-102", "TREX-103", "TREX-104"]Pętla while (true) nie zawiesza programu, bo generator pracuje leniwie: zatrzymuje się na yield i czeka na kolejne next(). Nigdy jednak nie rozwijaj nieskończonego generatora spreadem - takie wywołanie nie ma końca. Metoda padStart(3, "0") dopełnia numer zerami do trzech cyfr.
Praktyczny przykład: Stronicowanie danych
Iteratory świetnie nadają się do stronicowania (paginacji) dużych zbiorów danych, bo każda strona powstaje dopiero na żądanie:
1// Generator stron z danymi o dinozaurach
2function* paginateDinosaurs(dinosaurs, pageSize) {
3 for (let i = 0; i < dinosaurs.length; i += pageSize) {
4 yield {
5 page: Math.floor(i / pageSize) + 1,
6 data: dinosaurs.slice(i, i + pageSize),
7 hasMore: i + pageSize < dinosaurs.length
8 };
9 }
10}
11
12const allDinosaurs = [
13 "T-Rex", "Velociraptor", "Triceratops", "Stegosaurus",
14 "Brachiosaurus", "Pteranodon", "Mosasaurus", "Dilophosaurus",
15 "Gallimimus", "Parasaurolophus"
16];
17
18const pages = paginateDinosaurs(allDinosaurs, 3);
19
20console.log(pages.next().value);
21// { page: 1, data: ["T-Rex", "Velociraptor", "Triceratops"], hasMore: true }
22
23console.log(pages.next().value);
24// { page: 2, data: ["Stegosaurus", "Brachiosaurus", "Pteranodon"], hasMore: true }
25
26console.log(pages.next().value);
27// { page: 3, data: ["Mosasaurus", "Dilophosaurus", "Gallimimus"], hasMore: true }
28
29console.log(pages.next().value);
30// { page: 4, data: ["Parasaurolophus"], hasMore: false }Każde next() oddaje porcję trzech gatunków i flagę hasMore, która mówi, czy prosić o kolejną. Ostatnia strona ma jeden element, bo slice() nie wychodzi poza koniec tablicy.
Obiekt iterowalny z generatorem
Generatory upraszczają tworzenie obiektu iterowalnego. Zamiast ręcznie składać obiekt z metodą next(), wystarczy oznaczyć gwiazdką metodę [Symbol.iterator]:
1class DinosaurEnclosure {
2 constructor(name) {
3 this.name = name;
4 this.dinosaurs = [];
5 }
6
7 add(dinosaur) {
8 this.dinosaurs.push(dinosaur);
9 }
10
11 // Generator jako Symbol.iterator - znacznie prostsze!
12 *[Symbol.iterator]() {
13 for (const dino of this.dinosaurs) {
14 yield dino;
15 }
16 }
17}
18
19const paddock = new DinosaurEnclosure("Raptor Paddock");
20paddock.add({ name: "Blue", species: "Velociraptor" });
21paddock.add({ name: "Charlie", species: "Velociraptor" });
22paddock.add({ name: "Delta", species: "Velociraptor" });
23
24for (const raptor of paddock) {
25 console.log(`Raptor: ${raptor.name}`);
26}
27// Raptor: Blue
28// Raptor: Charlie
29// Raptor: Delta
30
31// Spread operator działa automatycznie
32const names = [...paddock].map(r => r.name);
33console.log(names); // ["Blue", "Charlie", "Delta"]Porównaj to z parkiem z wcześniejszej sekcji: tam kilkanaście linijek z indeksami, tutaj trzy. Obiekt generatora sam spełnia oba protokoły naraz, więc klasa od razu działa z for...of i ze spreadem.
Destrukturyzacja i spread
Z protokołu iterowalnego korzystają też spread i destrukturyzacja tablic - obie weszły do języka w ES6 (ES2015), podobnie jak destrukturyzacja obiektów:
1const raptors = ["Blue", "Charlie", "Delta", "Echo"];
2
3// Destrukturyzacja tablicy - kolejne wartości z iteratora
4const [first, second, third] = raptors;
5console.log(first, second, third); // Blue Charlie Delta
6
7// Spread - łączenie tablic
8const newcomers = ["Fox", "Ghost"];
9const allRaptors = [...raptors, ...newcomers];
10console.log(allRaptors.length); // 6
11
12// Destrukturyzacja i spread obiektu - bez iteratorów
13const raptor = { name: "Blue", age: 4, isAlpha: true };
14const { name, age, isAlpha } = raptor;
15const olderBlue = { ...raptor, age: 5 };
16console.log(name, age, isAlpha, olderBlue.age); // Blue 4 true 5Destrukturyzacja tablicy pobiera wartości kolejno z iteratora, dlatego zadziała też z parkiem czy generatorem. Obiekty to inna historia: destrukturyzacja obiektu sięga po właściwości według nazw, a spread obiektu, dodany w ES2018, kopiuje jego własne pola - żadne z nich nie używa Symbol.iterator. Kopia jest płytka, a oryginalny raptor nadal ma 4 lata.
Podsumowanie
Dr. Wu podsumowuje: "Iteratory to jak specjalistyczne sondy genetyczne - pozwalają przeglądać dane sekwencyjnie, krok po kroku:"
- Iterator - obiekt z metodą
next()zwracającą{ value, done } - Symbol.iterator - metoda, która czyni obiekt iterowalnym (działa z
for...of) - Generatory (
function*+yield) - upraszczają tworzenie iteratorów - Protokół iterowalny - umowa, że obiekt można iterować, używana przez
for...of, spread (...) i destrukturyzację
W codziennym kodzie polecam generator zamiast ręcznego next() - jest krótszy i trudniej w nim o błąd. Spread wróci w tej lokacji we wzorcu Builder, gdzie metoda build() oddaje kopię obiektu. W laboratorium poniżej zbudujesz klasę DinosaurEnclosure z generatorem jako Symbol.iterator.
Pamiętaj: iterator to sonda, która czyta próbki DNA po jednej, a Symbol.iterator to gniazdo, do którego pasuje każda pętla for...of.
Kod do tej lekcji: index.js
1// Iteratory i protokol iterowalny
2// Park Jurajski - Laboratorium genetyczne
3
4console.log("=== ITERATOR RECZNY ===");
5
6// Tworzenie prostego iteratora
7function createDinoIterator(dinosaurs) {
8 let index = 0;
9 return {
10 next() {
11 if (index < dinosaurs.length) {
12 return { value: dinosaurs[index++], done: false };
13 }
14 return { value: undefined, done: true };
15 }
16 };
17}
18
19const raptorIterator = createDinoIterator(["Blue", "Charlie", "Delta", "Echo"]);
20console.log(raptorIterator.next()); // { value: "Blue", done: false }
21console.log(raptorIterator.next()); // { value: "Charlie", done: false }
22console.log(raptorIterator.next()); // { value: "Delta", done: false }
23console.log(raptorIterator.next()); // { value: "Echo", done: false }
24console.log(raptorIterator.next()); // { value: undefined, done: true }
25
26console.log("\n=== SYMBOL.ITERATOR ===");
27
28// Obiekt iterowalny z Symbol.iterator
29const jurassicPark = {
30 sectors: [
31 { id: "A", dinosaurs: ["Rexy", "Blue"] },
32 { id: "B", dinosaurs: ["Spike", "Trike"] },
33 ],
34
35 [Symbol.iterator]() {
36 let sectorIndex = 0;
37 let dinoIndex = 0;
38 const sectors = this.sectors;
39
40 return {
41 next() {
42 while (sectorIndex < sectors.length) {
43 const sector = sectors[sectorIndex];
44 if (dinoIndex < sector.dinosaurs.length) {
45 const value = {
46 sector: sector.id,
47 dinosaur: sector.dinosaurs[dinoIndex]
48 };
49 dinoIndex++;
50 return { value, done: false };
51 }
52 sectorIndex++;
53 dinoIndex = 0;
54 }
55 return { value: undefined, done: true };
56 }
57 };
58 }
59};
60
61// for...of dziala dzieki Symbol.iterator
62for (const entry of jurassicPark) {
63 console.log(`Sektor ${entry.sector}: ${entry.dinosaur}`);
64}
65
66// Spread operator tez dziala
67const allDinos = [...jurassicPark];
68console.log("Wszystkie dinozaury:", allDinos);
69
70console.log("\n=== GENERATORY ===");
71
72// Generator - uproszczony sposob tworzenia iteratorow
73function* dinoIdGenerator(prefix, startFrom = 1) {
74 let id = startFrom;
75 while (true) {
76 yield `${prefix}-${String(id).padStart(3, "0")}`;
77 id++;
78 }
79}
80
81const raptorIds = dinoIdGenerator("RAPTOR");
82console.log(raptorIds.next().value); // "RAPTOR-001"
83console.log(raptorIds.next().value); // "RAPTOR-002"
84console.log(raptorIds.next().value); // "RAPTOR-003"
85
86// Generator stronicowania
87function* paginateDinosaurs(dinosaurs, pageSize) {
88 for (let i = 0; i < dinosaurs.length; i += pageSize) {
89 yield {
90 page: Math.floor(i / pageSize) + 1,
91 data: dinosaurs.slice(i, i + pageSize),
92 hasMore: i + pageSize < dinosaurs.length
93 };
94 }
95}
96
97const allSpecies = ["T-Rex", "Velociraptor", "Triceratops", "Stegosaurus",
98 "Brachiosaurus", "Pteranodon", "Mosasaurus", "Dilophosaurus"];
99
100const pages = paginateDinosaurs(allSpecies, 3);
101console.log("\nStronicowanie:");
102console.log(pages.next().value);
103console.log(pages.next().value);
104console.log(pages.next().value);
105
106// TODO: Stworz klase DinosaurEnclosure z generatorem jako Symbol.iterator
107// Klasa powinna miec metode add() i byc iterowalna
108console.log("\n=== Cwiczenie ===");
109console.log("Stworz klase DinosaurEnclosure - patrz TODO w kodzie");Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jaki obiekt musi zwracać metoda next() iteratora w JavaScript?
2. Co musi implementować obiekt, aby można go było użyć w pętli for...of?
To 2 z 4 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Użyj array destructuring do przypisania pierwszych trzech Velociraptorów do zmiennych.
- Edytor kodu
Wyciągnij name, age i isAlpha z obiektu raptor używając destructuring.
- Edytor kodu
Połącz dwie grupy Velociraptorów używając spread operator.
- Układanie w pionie
Uporządkuj wzorce projektowe obiektów od prostych do złożonych:
- Edytor kodu
Stwórz metody hunt(), communicate() i followPack() w obiekcie raptor.
- Edytor kodu
Stwórz obiekt paddock zawierający tablicę raptors i metody zarządzania.
- Układanie w pionie
Uporządkuj nowoczesne funkcje JavaScript chronologicznie według standardu:
- Układanie w pionie
Uporządkuj zarządzanie pamięcią dla obiektów i tablic:
- Edytor kodu
Stwórz zmienną przechowującą gatunek dinozaura
- Układanie w pionie
Uporządkuj proces wyboru struktury danych:
- Edytor kodu
Napisz funkcję obliczającą wiek skamieniałości