DMarlon Studio

Lesson 7 · 7 of 20

Iterators and generators · JavaScript

Produce values on demand in JavaScript with function* and yield, and make your own objects work with for...of through Symbol.iterator.

JavaScript · Iterators and generators in DMarlon Studio: the example's code and its output in the terminal

A generator function is declared with function* and, instead of returning a value and finishing, it pauses at every yield. Calling invoiceNumbers("INV", 7) runs nothing yet: it returns a generator. Each next() resumes the code up to the next yield and hands back a { value, done } object, as the third line of output shows.

That is why a generator can be infinite without hanging the program: the condition-less for loop in invoiceNumbers only moves on when someone asks for another number. It is a handy way to produce values on demand: numbering schemes, pages from an API or lines from a file.

The iteration protocol is what for...of, the ... operator and destructuring rely on: they ask the object for its [Symbol.iterator]() method. Playlist defines it as a generator (*[Symbol.iterator]()), so its songs can be looped with for...of and copied into an array with [...party].

countdown shows what lies underneath, without generators: an iterator is just an object with a next() method that returns { value, done }. When done is true, the loop stops. Because [Symbol.iterator]() returns this, the object is both an iterable and an iterator.

Code · main.js

// A generator pauses at each yield and resumes on the next call to next().function* invoiceNumbers(prefix, start) {  for (let n = start; ; n++) {    yield `${prefix}-${String(n).padStart(4, "0")}`;  }}const numbers = invoiceNumbers("INV", 7);console.log(numbers.next().value);console.log(numbers.next().value);console.log(numbers.next());// Any object with [Symbol.iterator] works with for...of and spread.class Playlist {  constructor(...songs) {    this.songs = songs;  }  *[Symbol.iterator]() {    for (const song of this.songs) yield song.toUpperCase();  }}const party = new Playlist("intro", "hit single", "encore");for (const song of party) console.log("Playing", song);console.log([...party].length, "songs in the playlist");// A hand-written iterator: an object whose next() returns { value, done }.function countdown(from) {  let current = from;  return {    [Symbol.iterator]() { return this; },    next: () => (current > 0 ? { value: current--, done: false } : { value: undefined, done: true }),  };}console.log("Launch in", [...countdown(3)].join(", "));

Output · node main.js

INV-0007
INV-0008
{ value: 'INV-0009', done: false }
Playing INTRO
Playing HIT SINGLE
Playing ENCORE
3 songs in the playlist
Launch in 3, 2, 1

Key points

  • function* defines a generator; yield hands out a value and pauses.
  • Each next() returns { value, done }.
  • A generator can be infinite because it only computes what is asked for.
  • An object with [Symbol.iterator]() works with for...of and ....

Exercise

Write a take(iterable, n) generator that yields only the first n values of another iterable, and use it to get five invoice numbers with [...take(invoiceNumbers("INV", 1), 5)].