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. Na szczęście JavaScript oferuje iteratory i protokół iterowalny - mechanizmy, które pozwalają kontrolować sposób przeglądania kolekcji danych.
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 - true jeśli nie ma więcej elementów, false w przeciwnym przypadku1// 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 }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 }Możemy sprawić, by dowolny obiekt był iterowalny - wystarczy zdefiniować metodę
[Symbol.iterator]():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); // 6Generatory 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}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"]Iteratory świetnie nadają się do stronicowania (paginacji) dużych zbiorów danych:
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 }Generatory upraszczają tworzenie obiektu iterowalnego:
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"]Dr. Wu podsumowuje: "Iteratory to jak specjalistyczne sondy genetyczne - pozwalają przeglądać dane sekwencyjnie, krok po kroku:"
next() zwracającą { value, done }for...of)function* + yield) - upraszczają tworzenie iteratorówfor...of, spread (...) i destrukturyzację