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]
 
JavaScript sandboxCtrl+Enter runs
// 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
 
JavaScript sandboxCtrl+Enter runs
// 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
 
JavaScript sandboxCtrl+Enter runs

Practice

Write a generator range(start, end, step = 1) that start, start + step, … while less than end, without ever building an array.

Exercise

A lazy range

3 checks
function* range(start, end, step = 1) {
  // yield values from start up to (not including) end
}

console.log([...range(0, 10, 3)])
 
JavaScript sandboxCtrl+Enter runs
Output
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

A custom iterable

3 checks
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])
 
JavaScript sandboxCtrl+Enter runs
Output
Press Run to try your code, then Check answer to grade it.

Check yourself

Quick check

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.