JavaScript ES6 generators
allow you to define functions that can be exited and later re-entered,
while retaining their context (variable bindings). They are defined using
function*
(function
keyword followed by an
asterisk) and use yield
expressions to return their result.
For example:
function* generateRange(end, start = 0, step = 1) {
let x = start - step;
while(x < end - step) yield x += step;
}
const gen5 = generateRange(5);
let x = gen5.next();
while (!x.done) {
console.log(x.value);
= gen5.next();
x // Logs: 0, 1, 2, 3, 4 }
In the above example, we define a generator function,
generateRange
, which will return each value between
start
and end
, incrementing by
step
each time. We use the
generator object
to call Generator.prototype.next()
until it returns
{value: undefined, done: true}
to iterate over the values the
generator produces.
Symbol.iterator
specifies the default iterator for an object. Oftentimes,
Symbol.iterator
is implemented using a generator function.
For example:
const iterableXx = {
Symbol.iterator]: function* () {
[yield 1;
yield 2;
};
}
console.log([...iterableX]); // [1, 2]
As you can see in this example, the object is made iterable by assigning a
generator function to its Symbol.iterator
property. This can
come especially handy, if you want to iterate over some arbitrary data or
create an object that is iterable and uses a generator function under the
hood.
Knowing how both concepts work, we can combine them to create a range generator, similar to Python or Ruby’s ranges:
const range = (end, start = 0, step = 1) => {
function* generateRange() {
let x = start - step;
while(x < end - step) yield x += step;
}return {
Symbol.iterator]: generateRange
[;
}
}
console.log([...range(7)]); // [0, 1, 2, 3, 4, 5, 6]
for (let i of range(8, 2, 2)) console.log(i); // Logs: 2, 4, 6