JavaScript has a standard for defining an object’s iteration behavior. In fact, it’s at work whenever you destructure an array or use it in a for..of
loop.
const arr = ['first', 'second', 'third'];
const [first, second, third] = arr;
for(const item of arr) {
doStuff(item);
}
But it’s not just for built-in objects. You can make any object iterable by giving it a Symbol.iterator
function property that returns an object adhering to the iterator protocol. In its most basic form, that returned object has next()
method returning another object with value
and done
properties.
For example, here’s an iterable that provides a range of numbers (1
through 3
):
let count = 1;
const iterableObj = {
[Symbol.iterator]() {
return {
next() {
return {
done: count === 4,
value: count++,
};
},
};
},
};
for (const item of iterableObj) {
console.log(item);
}
Correctly setting that done
property is critical, because that’s how the for
loop will know when to discontinue execution, or, if you’re destructuring, when you’ll start getting undefined
values:
const [a, b, c, d] = iterableObj;
While all that’s cool, there’s a notorious lack of “real” use cases for custom iterables in the wild. At least, that’s the story as I’ve experienced it.
And then I saw this tweet from Alex Reardon. In his example, an iterator and destructuring are used to generate an arbitrary number of objects on the fly (DOM elements, in his case). Here’s that implementation (a smidge modified, but the same spirit):
function getElements(tagName = 'div') {
return {
[Symbol.iterator]() {
return {
next() {
return {
done: false,
value: document.createElement(tagName),
};
},
};
},
};
}
const [el1, el2, el3] = getElements('div');
console.log(el1, el2, el3);
At first, it looked unnecessarily complicated, especially when considering what I’d personally otherwise do with an array:
function getElements(tagName = 'div', number) {
return new Array(number).fill(null).map(i => document.createElement(tagName));
}
const [el1, el2, el3] = getElements('div', 3);
console.log(el1, el2, el3);
Despite needing to specify how many items you’d like to generate up front, this sort of approach had always satisfied me (despite .fill()
always feeling a little weird). But then I remembered that there’s more than one way to make your own iterable in JavaScript.
If using Symbol.iterator
is too verbose, you can syntactically sweeten it with a generator, a special function returning a Generator
object that functions the same way as a hand-written iterable. Here’s that same method written as a generator function:
function* getElements(tagName = 'div') {
while (true) {
yield document.createElement(tagName);
}
}
const [el1, el2, el3] = getElements('div');
console.log(el1, el2, el3);
That infinite while
loop may be startling, but it’s safe — the yield
keyword won’t allow it to continue to run until the generator is invoked each time, whether it be via for..of
(which you can even use asynchronously), destructuring, or something else.
This change made it much more appealing to use iterables in destructuring items, for a few reasons:
- I don’t need to specify how many items I want to generate up front. As such, the function’s signature is a little simpler (the
number
parameter is no longer required). - There’s a (small) performance advantage. Items are produced on demand, not up front. I’ll never accidentally generate five items, end up only needing three, and leaving two orphans.
- In my opinion, it’s more elegant than filling an array. You can even get the function body down to a single line:
function* getElements(tagName = 'div') {
while (true) yield document.createElement(tagName);
}
And in addition to those benefits, an honorable mention: the increased social credibility that comes with telling people you’ve had a reason to use a generator.
When you’ve used a certain set of tools for countless jobs, it’s not easy to be given a new tool and expect to immediately know how it’s uniquely suited to solve certain problems. So, despite this being one of the first “practical” use cases I’ve for a generator, I don’t expect it to be the last. After all, it’s still a new tool for me.
That said, I’m eager to hear about other reasons iterators and generators are useful for a job, no matter how insignificant. If you have ‘em, yield
’em!