JavaScript generators
To bring you up to speed please read 👉 previous post about iterables.
In the spirit of learning by doing let’s refactor the Range class from the previous post to use the generator function.
Implementation of Range class
Here is the implementation using the generator function.
A couple of things to unpack:
- What’s the asterisk near the function keyword?
- What’s yield returning?
- What’s yield?
- Where’s return?
The function* denotes we are creating a generator function.
Because generators implement the same interface as iterables, yield is returning:
{ value: (yielded value), done: boolean }
Yield is a special keyword controlling execution. It returns a value from the generator and stops execution until the next yield keyword.
Now we have another question: How is returned value handled in iterables.
If we now try to access the returned value from generator2
by calling generator2.next()
we won’t get {value: 3, done: true}
.
We will get {value: undefined, done: true}
instead.
Why is that?
For of loop iterates iterable until property done: true
is returned. The value is already consumed
by for loop and thus not available in the next .next()
call.
Yield as an input
There is this one thing about the yield keyword we haven’t discussed yet.
Yield can bring data into the generator from outside scope.
The .next()
function can accept one argument that will be accessible during the current
execution step.
Let’s look at what’s going on during generator execution.
What code is being executed?
gen3.next(1)
Argument in the first next()
is discarded because we don't have a previous yield expression to evaluate it from.
⚠️ Variable a
is undefined at the moment.
gen3.next(2)
⚠️ Variable b
is undefined at the moment.
gen3.next(3)
⚠️ Variable c
is undefined at the moment.
gen3.next(4)
Stopping the generator
Generator object expose apart from next()
method also .throw()
and .return()
.
We can send either throw new Error(message)
or insert return
at the current suspension point inside the generator.
Returning is practical for early stopping. Throwing an error is useful for testing purposes when the generator can throw errors during execution and we want to simulate these errors from outside.
How are generators useful?
Generators are handy when you would like to use streams that are consumed by smaller chunks. They can more readable alternative to closures📎1.
Processing data in chunks reduces memory pressure but increases overall processing time.
Below is a code sample of more advanced usage of generators.
To learn more about what it does 👉 check the repo.
References
📎1Closure example:
Alternative to closure using a generator: