Lesson 15 of 18 · 0 of 2 exercises solved
Iterators, Generators & Symbols
You will learn the iteration protocol that for...of, and use, and how make it easy to implement. With it, your own objects can be looped over like .
// The iterable protocol: an object with a [Symbol.iterator] method
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from
const last = this.to
return {
next() {
return current <= last
? { value: current++, done: false }
: { value: undefined, done: true }
},
}
},
}
console.log([...range]) // [1, 2, 3, 4, 5]
for (const n of range) console.log(n)
const [first, second] = range // 1, 2
// Generators write the same thing in three lines
function* rangeGen(from, to, step = 1) {
for (let i = from; i <= to; i += step) yield i
}
console.log([...rangeGen(0, 10, 2)]) // [0, 2, 4, 6, 8, 10]
// Generators are lazy — nothing runs until you pull
function* idGenerator() {
let id = 1
while (true) yield id++ // infinite, but harmless
}
const ids = idGenerator()
console.log(ids.next().value) // 1
console.log(ids.next().value) // 2
// yield* delegates to another iterable
function* walk(node) {
yield node.value
for (const child of node.children ?? []) yield* walk(child)
}
const tree = {
value: 'root',
children: [
{ value: 'a', children: [{ value: 'a1' }] },
{ value: 'b' },
],
}
console.log([...walk(tree)]) // ['root', 'a', 'a1', 'b']
// Two-way: next(v) sends a value back INTO the generator
function* adder() {
let total = 0
while (true) {
const n = yield total // the yielded value goes out, next()'s arg comes in
total += n
}
}
const acc = adder()
acc.next() // prime it
console.log(acc.next(5).value) // 5
console.log(acc.next(3).value) // 8
// Async iteration — for await...of over a stream of promises
async function* fetchPages(url) {
let next = url
while (next) {
const res = await fetch(next)
const page = await res.json()
yield* page.items // hand out each item
next = page.nextUrl // then request the next page
}
}
for await (const item of fetchPages('/api/items?page=1')) {
console.log(item.id) // consumer never sees the pagination
}
// Symbols: unique keys that never collide with string keys
const CACHE = Symbol('cache')
class Repo {
constructor() { this[CACHE] = new Map() }
get(id) {
if (!this[CACHE].has(id)) this[CACHE].set(id, { id })
return this[CACHE].get(id)
}
}
const repo = new Repo()
console.log(Object.keys(repo)) // [] — symbol keys are hidden from enumeration
Practice
Write a generator range(start, end, step = 1) that start, start + step, … while less than end, without ever building an array.
Exercise
3 checksA lazy range
function* range(start, end, step = 1) {
// yield values from start up to (not including) end
}
console.log([...range(0, 10, 3)])
Press Run to try your code, then Check answer to grade it.Make Playlist by implementing [Symbol.iterator] as a generator method, so for...of and spread yield the track titles in order.
Exercise
3 checksA custom iterable
class Playlist {
constructor() {
this.tracks = []
}
add(title) {
this.tracks.push(title)
return this
}
// *[Symbol.iterator]() { ... }
}
const p = new Playlist().add('Intro').add('Chorus').add('Outro')
console.log([...p])
Press Run to try your code, then Check answer to grade it.Check yourself
What does for...of look for on the object it loops over?
Finished reading?2 exercises still open. You can come back to them later.
Tip: use the ← and → keys to move between lessons.