The Iteration Protocols: Symbol.iterator, Iterables & Generator Functions
Unlock ECMAScript's iteration protocol. Create custom iterables, master generator functions (function*), and stream data lazily with yield.
By the end, you’ll be able to…
- Implement the Iterable and Iterator protocols using Symbol.iterator and next().
- Author generator functions (function*) and manage execution pauses via yield.
- Stream infinite sequences and handle large datasets with constant O(1) memory.
01The Universal Iteration Protocols
Prior to ES2015, iterating over different data structures required disparate techniques: for-loops for arrays, for...in for objects, and index counting for DOM NodeLists. ES2015 unified iteration across the entire language through two formal contracts: the Iterable Protocol and the Iterator Protocol.
An object is Iterable if it implements a method whose key is Symbol.iterator. This method must return an Iterator object. An Iterator object implements a .next() method that returns an object with two properties: value (the current datum) and done (a boolean indicating if iteration has concluded). Any object implementing this contract automatically works with for...of loops, the spread operator (...), and Array.from().
The ECMAScript Iteration Protocol is composed of two distinct interfaces: the Iterable Protocol and the Iterator Protocol. An object is an Iterable if it implements a method keyed by Symbol.iterator that returns an Iterator.
An Iterator is an object that implements a .next() method returning a result descriptor ({ value: any, done: boolean }). This uniform interface allows the JavaScript runtime to consume arrays, strings, Maps, Sets, and custom streams using identical syntax: for...of loops, the spread operator (...), and Array.from().
02Generator Functions: Pausing Execution with Yield
Writing custom iterator objects with manual state tracking can be verbose. ECMAScript provides Generator Functions (declared as function*) as a syntax-level mechanism to write iterators effortlessly.
When you invoke a generator function, it does not execute its body immediately. Instead, it returns a Generator object, which conforms to both the iterable and iterator protocols. When .next() is called, the generator executes until it encounters a yield expression. It suspends execution, yields the value to the caller, and remembers its exact execution frame. Calling .next() again resumes execution immediately following the yield statement.
03Lazy Evaluation and Memory Optimization
The superpowers of generators are lazy evaluation and pulling data on-demand. Standard arrays eagerly compute and store all elements in memory immediately. If you need to process a 5-gigabyte log file with 10 million entries, attempting to load all lines into a standard array will crash with a memory heap exhaustion.
With a generator, values are computed only when requested by the consumer. Memory consumption remains strictly constant (O(1)) regardless of whether the dataset contains 10 items or 10 billion items.
04Why This Matters in Production
Generators are the hidden engine behind modern pagination streams, asynchronous saga architectures (Redux-Saga), and streaming Node.js pipelines. They allow you to write synchronous-looking code that pauses and resumes gracefully.
Whenever you are designing APIs that process unbounded streams of events, database cursor batches, or infinite sequences, build them with generators to ensure zero memory bloat.
The ideas worth keeping.
- Implement [Symbol.iterator] to make any custom data structure compatible with for...of and spread (...).
- Generator functions (function*) pause execution with yield and resume seamlessly with .next().
- Use generators for streaming and lazy evaluation to handle massive datasets with constant O(1) memory.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Exercise 1: Infinite Fibonacci Generator
Write a generator function fibonacciGenerator that yields Fibonacci numbers indefinitely on demand.
Explore a worked solution
function* fibonacciGenerator() { let a = 0, b = 1; while (true) { yield a; [a, b] = [b, a + b]; }}The generator maintains a and b across yields, evaluating the sequence lazily as consumers invoke .next().
Exercise 2: Custom Iterable Tree Traversal
Implement [Symbol.iterator] on a Tree node object to traverse values in depth-first order.
Explore a worked solution
class TreeNode { constructor(val, children = []) { this.val = val; this.children = children; } *[Symbol.iterator]() { yield this.val; for (const child of this.children) { yield* child; } }}Using yield* delegates iteration recursively to child iterables, creating a clean depth-first traversal with for...of.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Exercise 1: Infinite Fibonacci Generator
Write a generator function fibonacciGenerator that yields Fibonacci numbers indefinitely on demand.
[Process exited with code 0 - No output]
Keep exploring.
The official references behind this lesson, when you want a second angle or the full details.
One lesson closer.
Reading is a start. Trying it yourself makes the connection.