In the genetics laboratory of Jurassic Park, Dr. Henry Wu is reviewing enormous collections of dinosaur DNA data. "We need a way to process genetic sequences element by element, without loading the entire database into memory," he says. Fortunately, JavaScript offers iterators and the iterable protocol — mechanisms that let you control how collections of data are traversed.
An iterator is an object that knows how to access the elements of a collection one at a time, tracking its current position. An iterator must implement a
next() method that returns an object with two properties:value — the current valuedone — true if there are no more elements, false otherwise1// Simple iterator — created manually
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 }For an object to be usable in a
for...of loop, it must implement the iterable protocol — meaning it must have a [Symbol.iterator]() method that returns an iterator.Many built-in types already implement this:
Array, String, Map, Set.1// Arrays are iterable
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// Strings are also iterable
12for (const char of "DINO") {
13 console.log(char); // D, I, N, O
14}
15
16// We can manually retrieve an iterator from an array
17const iterator = species[Symbol.iterator]();
18console.log(iterator.next()); // { value: "T-Rex", done: false }
19console.log(iterator.next()); // { value: "Velociraptor", done: false }We can make any object iterable — all we need to do is define a
[Symbol.iterator]() method:1// Jurassic Park as an iterable object
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 // Implementation of the iterable protocol
11 [Symbol.iterator]() {
12 let sectorIndex = 0;
13 let dinoIndex = 0;
14 const sectors = this.sectors;
15
16 return {
17 next() {
18 // Traverse all dinosaurs in all sectors
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// Now we can iterate over the park!
39for (const entry of jurassicPark) {
40 console.log(`Sector ${entry.sector}: ${entry.dinosaur}`);
41}
42// Sector A: Rexy
43// Sector A: Blue
44// Sector B: Spike
45// Sector B: Trike
46// Sector C: Pteranodon
47// Sector C: Mosasaurus
48
49// Spread operator and destructuring also work
50const allDinos = [...jurassicPark];
51console.log(allDinos.length); // 6Generators are special functions marked with an asterisk (
function*) that simplify creating iterators. They use the yield keyword to "produce" successive values:1// DNA sequence generator
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// Generators are iterable — they work with for...of
14for (const nucleotide of dnaSequenceGenerator("GCTA")) {
15 console.log(nucleotide); // G, C, T, A
16}1// Dinosaur ID generator
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// Infinite generator — but we only take what we need
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"]Iterators are great for paginating large datasets:
1// Page generator for dinosaur data
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 }Generators simplify creating iterable objects:
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 as Symbol.iterator — much simpler!
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 works automatically
32const names = [...paddock].map(r => r.name);
33console.log(names); // ["Blue", "Charlie", "Delta"]Dr. Wu sums up: "Iterators are like specialized genetic probes — they let you scan data sequentially, step by step:"
next() method returning { value, done }for...of)function* + yield) — simplify creating iteratorsfor...of, spread (...), and destructuring