Iterators, Generators and Symbols

Learn how for...of really works, make your own objects iterable with Symbol.iterator, write generator functions with yield, and use symbols as unique keys.

Lesson 54 of 56Advanced16 min read

You've used for...of on arrays, strings, Sets and Maps, and spread syntax like [...set] to turn things into arrays. But how do they know how to loop over such different kinds of data? They all follow the same simple agreement, the iteration protocol: an object is iterable if it can hand out an iterator, and an iterator is anything with a next() method that returns the values one at a time.

Once you understand that agreement, you can make your own objects work with for...of, spread and destructuring. Generator functions, written function*, make this remarkably easy: they can pause in the middle with yield, hand back a value, and carry on from the same spot next time. That makes them perfect for sequences, step-by-step processes and even infinite lists that are calculated only when needed.

The key that connects everything is a symbol, Symbol.iterator. Symbols are a primitive type designed for unique property keys that never clash. In this lesson you'll learn all three ideas, and see how they lead into async iteration with async/await.

The iteration protocol

JavaScript
const it = ["a", "b"][Symbol.iterator]();
it.next(); // { value: "a", done: false }
it.next(); // { value: "b", done: false }
it.next(); // { value: undefined, done: true }
  • An iterator has a next() method that returns { value, done }.
  • An iterable has a [Symbol.iterator]() method that returns a new iterator.
  • for...of, spread [...x], destructuring const [a, b] = x, Array.from(x), new Set(x) and Promise.all(x) all work with any iterable.
  • Plain objects aren't iterable, which is why for...of on {} throws an error.

Generator functions

JavaScript
function* countTo(n) {
  for (let i = 1; i <= n; i++) {
    yield i;      // pause here and hand out i
  }
}

for (const x of countTo(3)) console.log(x); // 1, 2, 3
  • function* declares a generator. Calling it doesn't run the body; it returns a generator object, which is both an iterator and an iterable.
  • Each next() runs the body until the next yield, returns that value, and pauses.
  • When the function ends, done becomes true.
  • yield* hands out every value from another iterable: yield* [1, 2, 3].

Lazy and infinite sequences

Generators only work out the next value when it's asked for. That makes infinite sequences possible:

JavaScript
function* ids() { let n = 1; while (true) yield `id-${n++}`; }
const next = ids();
next.next().value; // "id-1"
next.next().value; // "id-2"

The while (true) is safe because the generator pauses at every yield. Just never spread an infinite generator into an array. Newer browsers add iterator helpers such as .take(5), .map() and .filter() directly on iterators, so you can work with lazy sequences without creating arrays.

Symbols

A symbol is a unique value created with Symbol("description"). No two symbols are ever equal, even with the same description, so they make property keys that can't clash with anyone else's:

JavaScript
const id = Symbol("id");
const user = { name: "Ana", [id]: 42 };
user[id];            // 42
Object.keys(user);   // ["name"]: symbol keys are skipped

JavaScript uses built-in well-known symbols to let you customise behaviour: Symbol.iterator for looping, Symbol.asyncIterator for for await...of, and Symbol.toPrimitive for converting objects to numbers or strings.

JavaScript Iterators and Generators Syntax

Iterators and generators

function* gen() { yield 1; yield 2; }
const g = gen();
g.next();            // { value: 1, done: false }
[...gen()];          // [1, 2]

const range = {
  from: 1, to: 3,
  *[Symbol.iterator]() { for (let i = this.from; i <= this.to; i++) yield i; },
};
[...range];          // [1, 2, 3]
const key = Symbol("key");

JavaScript Iterators and Generators Summary

  • An iterator has next() returning { value, done }; an iterable has [Symbol.iterator]().
  • for...of, spread and destructuring work with any iterable.
  • function* creates generators that pause at each yield.
  • Generators are lazy, so they can represent infinite sequences.
  • Symbols are unique values, used for keys that never clash and built-in hooks like Symbol.iterator.

JavaScript Iterators and Generators Examples

Each example runs in the page. Edit the code and press Run to see what changes.

Example 1

Driving an iterator by hand

const letters = new Set(["x", "y"]);
const it = letters[Symbol.iterator]();

console.log(it.next());
console.log(it.next());
console.log(it.next());

function* steps() {
  console.log("  (start)");
  yield "first";
  console.log("  (resumed)");
  yield "second";
  console.log("  (finished)");
}

const g = steps();
console.log("Created, nothing ran yet");
console.log(g.next().value);
console.log(g.next().value);
console.log(g.next().done);
ConsoleEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Line 2Asking a Set for its iterator.
  2. Lines 4 to 6Each next() returns the next value, then done: true.
  3. Lines 8 to 14A generator that logs as it runs, so you can see where it pauses.
  4. Lines 16 to 17Calling steps() runs none of the body.
  5. Lines 18 to 20Each next() runs only up to the next yield.
Example 2

Making your own object iterable

const playlist = {
  name: "Road trip",
  songs: ["Intro", "Highway", "Sunset", "Home"],
  *[Symbol.iterator]() {
    for (const [i, song] of this.songs.entries()) {
      yield `${i + 1}. ${song}`;
    }
  },
};

for (const line of playlist) console.log(line);

const [first, second] = playlist;
console.log("First two:", first, "|", second);
console.log("Count:", [...playlist].length);
ConsoleEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Line 4A generator method as Symbol.iterator makes the object iterable.
  2. Lines 5 to 7Each song is handed out with its number.
  3. Line 11for...of now works on the playlist.
  4. Line 13So does array destructuring.
  5. Line 15And spread.
Example 3

Lazy and infinite sequences

function* fibonacci() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

function* take(iterable, n) {
  if (n <= 0) return;
  for (const value of iterable) {
    yield value;
    if (--n === 0) return;
  }
}

console.log([...take(fibonacci(), 10)]);

const tag = Symbol("tag");
const item = { name: "Lamp", [tag]: "internal" };
console.log(Object.keys(item), item[tag], Symbol("a") === Symbol("a"));
ConsoleEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Lines 3 to 6An endless sequence; each value is only calculated when requested.
  2. Lines 9 to 15A helper generator that stops after n values.
  3. Line 17Only the first ten numbers are ever calculated.
  4. Lines 19 to 21Symbol keys are hidden from Object.keys, and every symbol is unique.

Common JavaScript Iterators and Generators Mistakes

Expecting a generator to run when called

Don't
function* setup() { console.log("ready"); yield; }
setup(); // logs nothing
Do
setup().next(); // logs "ready"

Spreading an infinite generator

Don't
const all = [...naturalNumbers()]; // never finishes
Do
const firstTen = [...take(naturalNumbers(), 10)];

Reusing a finished generator

Don't
const g = countTo(3);
[...g]; // [1, 2, 3]
[...g]; // []: the generator is already finished
Do
[...countTo(3)];
[...countTo(3)]; // call the generator function again for a fresh one

JavaScript Iterators and Generators in Real Projects

Where you'll meet these

Redux-Saga uses generators to describe step-by-step side effects. Database drivers and the Fetch API's streams use async iterators so you can process data in chunks with for await...of. Libraries mark private data with symbols to avoid clashing with your properties, and every built-in collection, from NodeLists to Maps, works with for...of through Symbol.iterator.

JavaScript Iterators and Generators Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Hand out each value from the generator.

function* colours() {
   "red";
   "green";
}
console.log([...colours()]);
Predict the outputmedium

What is logged?

function* g() { yield 1; yield 2; return 3; }
const it = g();
console.log(it.next().value, [...it].length);
Multiple choiceeasy

What does an iterator's next() return?

Multiple choicemedium

Which key makes an object work with for...of?

Fix the bugmedium

range(start, end) should be a generator that yields every whole number from start to end inclusive, so [...range(2, 5)] is [2, 3, 4, 5]. Fix it.

function range(start, end) {
  for (let i = start; i < end; i++) {
    return i;
  }
}
console.log([...range(2, 5)]);
ConsoleEdit the code, then press Run or Ctrl+Enter
Checks
  • Yields an inclusive range
  • Single value range
  • Is a generator

JavaScript Iterators and Generators Quiz

5 questions to check what you learned.

  1. How do you declare a generator function?

    • function gen()
    • function* gen()
    • generator gen()
    • async gen()
  2. Why can a generator use while (true) safely?

    • It pauses at every yield and only continues when asked
    • Browsers stop it after 1000 loops
    • while (true) never runs in generators
    • It runs in a separate thread
  3. Are two symbols with the same description equal?

    • Yes
    • No, every symbol is unique
    • Only in strict mode
    • Only if they're global
  4. Which of these is NOT iterable by default?

    • A string
    • A Map
    • A generator object
    • A plain object
  5. What does yield* do?

    • Hands out every value from another iterable in turn
    • Yields a star character
    • Ends the generator
    • Doubles the value

JavaScript Iterators and Generators Challenge

A paginated list

Create an object pages with:

  1. An items array: ["a", "b", "c", "d", "e"]
  2. A size of 2
  3. A generator method [Symbol.iterator] that yields the items in chunks of size, as arrays

So [...pages] should be [["a", "b"], ["c", "d"], ["e"]].

const pages = {

};

console.log([...pages]);
ConsoleEdit the code, then press Run or Ctrl+Enter
Checks
  • Yields chunks
  • Respects the size
  • Uses Symbol.iterator