Iterables and Generators

From Symbols To Iteration

In our last lesson, we explored how Symbols act as unique keys for object properties. We also saw that Well-Known Symbols allow us to change how JavaScript handles our objects behind the scenes. This lesson builds directly on that idea. We are going to look at one of the most powerful Well-Known Symbols: Symbol.iterator.

By the end of this lesson, you will know how to take a standard JavaScript class and make it "iterable." This means your custom objects will gain the ability to work with common tools like for...of loops, the spread operator, and array destructuring. Just like Symbol.toPrimitive helped us control how an object becomes a number or string, Symbol.iterator helps us control how an object shares its data one piece at a time.

The Iteration Protocol

JavaScript has a set of rules called the Iteration Protocol. For an object to be used in a loop like for...of, it must follow these rules. Specifically, the object needs a method named Symbol.iterator. When you try to loop over an object, JavaScript looks for this "magic" symbol key. If it finds it, the engine calls that method to get an iterator.

Think of an iterator as a specialized pointer that knows how to move through a collection of data. This protocol is what makes arrays and strings work with loops automatically. By adding this symbol to your own objects, you are telling the JavaScript engine that your object is now a collection that can be stepped through. This makes your custom code feel like a built-in part of the language.

Building A Custom Iterable Class

Let's look at how to implement this in a real class. Imagine we have a TransactionLog that stores several transactions. We want to be able to loop through these entries easily. In the example below, we use a private field called #entries to store our data and then define the iterator method.

class TransactionLog {
  #entries = [];
  add(entry) { this.#entries.push(entry); return this; }

  *[Symbol.iterator]() {
    for (const e of this.#entries) yield e;
  }
}

In this code, the asterisk * before the [Symbol.iterator] name tells JavaScript that this is a generator method. Inside the method, we use the yield keyword. You can think of yield as a way to pause the function and hand out a value. When the loop asks for the next item, the function resumes exactly where it left off until it hits the next yield. This is much simpler than manually tracking index numbers.

Consuming Iterables: for...of, Spread, And Destructuring

Once you have defined the Symbol.iterator method, your object can be used in several different ways. You aren't limited to just loops. Any JavaScript feature that expects a collection will now work with your object.

const log = new TransactionLog()
  .add({ type: "deposit",  amount: 200 })
  .add({ type: "withdraw", amount: 50 });

for (const e of log) console.log("entry:", e);
console.log("spread:", [...log]);
const [first, second] = log;
console.log("destructured:", first, second);

Output:

entry: { type: 'deposit', amount: 200 }
entry: { type: 'withdraw', amount: 50 }
spread: [ { type: 'deposit', amount: 200 }, { type: 'withdraw', amount: 50 } ]
destructured: { type: 'deposit', amount: 200 } { type: 'withdraw', amount: 50 }

The for...of loop uses the iterator to print each transaction. The spread operator [...] uses the iterator to pull every value out of the log and put it into a new array. Even array destructuring works by taking the first two items yielded by the iterator and assigning them to the variables first and second. This makes your custom classes very flexible and easy for other developers to use.

Standalone Generator Functions

Generators do not have to live inside a class. You can create standalone functions using the function* syntax. These are perfect for creating sequences of data on the fly, such as a range of numbers. This allows you to generate data only when it is actually needed, rather than creating a massive array in memory.

function* range(start, end, step = 1) {
  for (let i = start; i < end; i += step) yield i;
}

console.log("array:", [...range(1, 6)]); 

Output:

array: [1, 2, 3, 4, 5]

The range function looks like a normal loop, but the yield keyword turns it into a generator. When you call range(1, 6), it doesn't actually run the loop immediately. Instead, it returns an iterator object. The loop only runs as you pull values out of that object. In the example above, the spread operator pulls all values from 1 to 5 into an array.

The Iterator Object And Lazy Evaluation

To understand what is happening under the hood, we can call the iterator's next() method manually. Each time you call next(), JavaScript returns an object with two properties: value and done. The value is the data being yielded, and done is a true or false value that tells us if the sequence is finished.

const it = range(0, 3);
console.log(it.next()); 
console.log(it.next()); 
console.log(it.next()); 
console.log(it.next()); 

Output:

{ value: 0, done: false }
{ value: 1, done: false }
{ value: 2, done: false }
{ value: undefined, done: true }

This behavior is known as lazy evaluation. The generator does not do any work until you ask for the next value. On the first three calls, done is false because there are more numbers to provide. On the fourth call, the loop inside the generator has finished, so it returns undefined and sets done to true. This efficiency is very useful when dealing with large amounts of data where you might not need every single item at once.

Summary And Practice Preview

In this lesson, we moved from simply naming properties with Symbols to using them to control how objects behave during iteration. We learned that Symbol.iterator is the key that unlocks features like for...of and the spread operator. We also explored how generators and the yield keyword make it easy to produce sequences of data lazily.

As you move into the practice exercises, you will practice building your own iterables and standalone generators. I will see you in the practice area to put these new skills to work!

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal