Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Iteratory i protokół iterowalny

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.

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
    -
    true
    jeśli nie ma więcej elementów,
    false
    w 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 }

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 }

Tworzenie własnego obiektu iterowalnego

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); // 6

Generatory jako iteratory

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}

Generator z logiką

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"]

Praktyczny przykład: Stronicowanie danych

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 }

Obiekt iterowalny z generatorem

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"]

Podsumowanie

Dr. Wu podsumowuje: "Iteratory to jak specjalistyczne sondy genetyczne - pozwalają przeglądać dane sekwencyjnie, krok po kroku:"

  1. Iterator - obiekt z metodą
    next()
    zwracającą
    { value, done }
  2. Symbol.iterator - metoda, która czyni obiekt iterowalnym (działa z
    for...of
    )
  3. Generatory (
    function*
    +
    yield
    ) - upraszczają tworzenie iteratorów
  4. Protokół iterowalny - umowa, że obiekt można iterować, używana przez
    for...of
    , spread (
    ...
    ) i destrukturyzację
Ir a CodeWorlds