Alistair
Great article and great idea!!!
You could also create a generic generator that takes a function as a param that it runs to create each item. That way you only need a single flexible generator for creating any type of anything
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'];
// Using destructuring:
const [first, second, third] = arr;
// Using a `for..of` loop:
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);
}
// 1
// 2
// 3
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;
// 1, 2, 3, undefined
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);
// HTMLDivElement, HTMLDivElement, HTMLDivElement
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);
// HTMLDivElement, HTMLDivElement, HTMLDivElement
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:
number
parameter is no longer required).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!
Get blog posts like this in your inbox.
May be irregular. Unsubscribe whenever.Great article and great idea!!!
You could also create a generic generator that takes a function as a param that it runs to create each item. That way you only need a single flexible generator for creating any type of anything
I didnt know that the array deconstruction could be invoked on generators functions. That is a nice one to have knowledge of. I find it really rare to use generator functions, but they're pretty useful on some cases, so thanks for bringing it up. :D
Just for knowledge, a more suitable method would be:
function getElements(tagName, length) {
return Array.from({ length }, () => document.createElement(tagName));
}
Interesting. I know about generators and I am interested on them. I used to proceed with Array.from to convert an iterator to an array. I may try destructuring instead.
I have personally made an iterator for walk throug a node tree. I can use it to search for a node, make a node tree based in files folder structure, find out a node level... Well is very usefull for me, if somebody is interested can find me in GitHub : petazeta
Good article. If you want to see a very wide use case where generator functions and custom iterables shine, have a look at a the "js-coroutines" module on NPM (note: I am not the author, I just use generators a lot and I'm impressed by it). It's a tool for co-operative scheduling and it offers methods for writing generator-based high-priority sequential animations, as well as methods for writing low-priority incremental computations (as well as primitives for sorting data, LZ compressing JSON etc). You could use it to keep animations snappy at 60FPS in a scenario where a lot of number crunching needs to be done (perfect example being something like a spreadsheet widget with localstorage persistance or a similarly complex UI element).
fwiw, Array(num).fill(elem) would create an array with the exact same dom element in each slot. In that example, you'd actually want to do
Array(num).fill(anything).map(() => document.createElement('div'))
Love generators! I tried to use those for all of the advent of code projects this past December.
Whoa, great catch! I assumed each node would be unique, but totally makes sense that it doesn't. Gonna update this.