Numeric range generator in JavaScript
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. But how can one use them to create a range generator that produces a sequence of numbers?
A simple range generator
As the control flow of generators is tricky to grasp at first, let's start with the simplest possible range implementation. We'll only account for an end
value, starting at 0
and incrementing by 1
. What we need to do to accomplish this is to use a for
loop and yield
the value of i
at each iteration.
const range = function* (end) { for (let i = 0; i < end; i++) yield i; }; for (let i of range(5)) console.log(i); // Logs: 0, 1, 2, 3, 4
Consuming the generator output
Consuming the output of a generator can be done step-by-step, as needed, or all at once. Depending on our needs we have a few available options.
Consumption as needed
We can, for example, use the generator object to call Generator.prototype.next()
until it returns { value: undefined, done: true }
to iterate over the values the generator produces.
const gen5 = range(5); let x = gen5.next(); while (!x.done) { console.log(x.value); x = gen5.next(); } // Logs: 0, 1, 2, 3, 4
Iterating over the output
As shown in the previous example, we can also use for...of
to iterate over the generator's output. This is due to the fact that generators are iterable objects, implementing Symbol.iterator
under the hood.
for (let i of range(5)) console.log(i); // Logs: 0, 1, 2, 3, 4
Generator output as an array
Finally, we can also use the spread operator (...
) to consume the generator's output all at once. This is especially useful when you want to convert the generator's output into an array.
console.log([...range(5)]); // Logs: [0, 1, 2, 3, 4]
Adding start and step parameters
Having implemented the most basic version of a range generator, we can now extend it to accept start
and step
parameters. This will allow us to define a range that starts at a specific value and increments by a specific step.
In order to match the behavior of Python's range()
function, we need to account for the following scenarios:
- If only one argument is provided, it should be treated as the
end
value, withstart
defaulting to0
andstep
to1
. - If two arguments are provided, they should be treated as
start
andend
, withstep
defaulting to1
. - If three arguments are provided, they should be treated as
start
,end
, andstep
.
const range = function* (start, end, step = 1) { if (end === undefined) (end = start), (start = 0); for (let i = start; i < end; i += step) yield i; }; [...range(4)]; // [0, 1, 2, 3] (scenario 1) [...range(6, 10)]; // [6, 7, 8, 9] (scenario 2) [...range(2, 10, 2)]; // [2, 4, 6, 8] (scenario 3)