Blog

Make Any JavaScript Object Work With for…of

Arrays, strings, Maps and Sets all work with for...of. Plain objects do not. The difference is one method, and you can add it yourself.

The method has a Symbol for a name, Symbol.iterator, and for...of looks it up before doing anything else. If it is missing, you get a TypeError.

What happens without it

Looping over a plain object with for...of fails.

// plain object is not iterable
const plain = { a: 1, b: 2 };
try { for (const x of plain) console.log(x); }
catch (err) { console.log(err.constructor.name + ':', err.message); }

It prints:

TypeError: plain is not iterable

The message is accurate. The object is not iterable, because nothing at Symbol.iterator tells JavaScript how to walk it.

The protocol

Symbol.iterator must return an object with a next() method. Each call to next() returns { value, done }. When done is true, the loop stops.

// add Symbol.iterator
const bag = {
  items: ['pen', 'book', 'cup'],
  [Symbol.iterator]() {
    let i = 0;
    const items = this.items;
    return {
      next() {
        return i < items.length
          ? { value: items[i++], done: false }
          : { value: undefined, done: true };
      }
    };
  }
};
for (const item of bag) console.log(item);

It prints:

pen
book
cup

The closure over i holds the position between calls. Every call to Symbol.iterator makes a fresh one, so two loops over the same object do not interfere.

Everything else starts working too

for...of is only one of the things that reads this protocol. Spread, Array.from and array destructuring all use the same method.

// everything that reads iterables now works
console.log('spread    ->', [...bag]);
console.log('Array.from ->', Array.from(bag));
const [first, ...rest] = bag;
console.log('destructure ->', first, rest);

It prints:

spread    -> [ 'pen', 'book', 'cup' ]
Array.from -> [ 'pen', 'book', 'cup' ]
destructure -> pen [ 'book', 'cup' ]

You wrote one method and got four features.

The shorter version

Writing next() by hand gets tedious. A generator does the bookkeeping for you, and the star goes before the computed name.

// same thing with a generator
const range = {
  from: 1,
  to: 5,
  *[Symbol.iterator]() {
    for (let n = this.from; n <= this.to; n++) yield n;
  }
};
console.log([...range]);

// a class
class Deck {
  constructor(cards) { this.cards = cards; }
  *[Symbol.iterator]() { yield* this.cards; }
}
console.log([...new Deck(['A', 'K', 'Q'])]);

It prints:

[ 1, 2, 3, 4, 5 ]
[ 'A', 'K', 'Q' ]

Inside a class it looks the same. yield* hands off to another iterable, which here is just the array of cards.

What to remember

  • for...of looks for a method named Symbol.iterator.
  • That method returns an object with next(), and next() returns { value, done }.
  • Spread, Array.from and destructuring use the same method.
  • A generator method is the short way to write it.

The protocol is small on purpose. Anything that can produce values one at a time can present itself as a sequence, including things that are not stored in memory at all.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.

Leave a Reply