Generator functions are a special type of function that can be paused and resumed, allowing for more control over the flow of execution. Here are the key things to know about generator functions.
A generator function is defined by an *
at the end of the function
keyword.
function* generatorFunction() {}
//OR
const generatorFunction = function*() {}
A generator function returns a Generator
object. The Generator
object returned by the function is an iterator
. An iterator
object has a method called next()
which is used to iterate through a sequence of values. The next()
method returns an object with value
and done
properties. Value would be the current value that was returned and done
is a boolean that indicates if the iterator has stopped or if it’s still running.
Best way to understand the above is to see an example:
// Declare a generator function with a single return value
function* generatorFunction() {
return 'Hello, World!'
}
// Assign the Generator object to generator
const generator = generatorFunction();
// Call the next method on the Generator object
generator.next(); //Output: {value: "Hello, World!", done: true}
The value returned from calling next()
is Hello, World!
, and the state of done
is true
. Because this value came from a return
that closed out the iterator. Just like how in a normal function, where you write a return
statement the compiler will exit that function so it’s the same here.
But then what makes generator functions different? Generators introduced a new keyword called yield
. A yield
can pause a generator function & return a value. Here is an example first:
// A generator function with multiple yields and a return
function* generatorFunction() {
yield 'Value 1'
yield 'Value 2'
yield 'Value 3'
return 'Value 4'
}
const generator = generatorFunction()
// Call next four times
generator.next() // {value: "Value 1", done: false}
generator.next() // {value: "Value 2", done: false}
generator.next() // {value: "Value 3", done: false}
generator.next() // {value: "Value 4", done: true}
In the above example, we’re pausing the generator function 3 times with different values, and returning a value at the end. Then we’ll assign our Generator
object to a generator
variable.
Then we call next()
on the generator function, and it’ll pause every time it encounters a yield
. done
will be false
after each yield
, indicating the generator has not finished. Once it encounters a return
, or there are no more yield
s encountered in the function, done
will flip to true
, and the generator will be finished.
The return
is not a mandatory thing for a generator function. If you don’t write it then the done
the value will remain false for 4 iterations and then it’ll be true
on the fifth iteration i.e. {value: undefined, done: true}
.
To sum up the above theory, yield
pause until a value is returned and then go to the next command and return finishes the function.
Just like an Array
we can also loop through a Generator
object.
for (const value of generator) { console.log(value) }
//Output:
// Value 1
// Value 2
// Value 3
You can also create an array with the values returned from a generator object:
const values = [...generator];
console.log(values)
//Output:
// ["Value 1", "Value 2", "Value 3"]
You might have noticed that both for...of
and ...
(spread operator) are not factoring in the Value 4
which using return
and it’s only considering the values it got with yield
.
We can also force stop a generator. See an example below:
function* generatorFunction() {
yield 'Neo'
yield 'Morpheus'
yield 'Trinity'
}
const generator = generatorFunction()
generator.next()
generator.return('There is no spoon!')
generator.next()
// Output
// {value: "Neo", done: false}
// {value: "There is no spoon!", done: true}
// {value: undefined, done: true}
You can even keep the return
empty like return()
that would also force the Generator
object to complete and ignore any other yield
keywords.
This type of behavior is useful when we’re making let’s say cancel a web request when the user wants to perform a different action, as it is not possible to directly cancel a Promise.
If the body of the generator has a way to catch errors, you can use throw()
method to throw an error into the generator. Upon throwing an error, the generator gets terminated as well.
function* generatorFunction() {
try { yield 'Value 1'
yield 'Value 2'
} catch (error) { console.log(error)}
}
const generator = generatorFunction()
generator.next()
generator.throw(new Error('This is an error!'))
// Output
// {value: "Value 1", done: false}
// Error: This is an error!
// {value: undefined, done: true}
So there are three methods of Generator
object:
next()
– Returns the next value in a generatorreturn()
– Returns a value in a generator and finished the generatorthrow()
– Throws an error and finished the generator
Similarly, a Generator
object can be in one of these possible states:
suspended
Generator has halted execution but has not terminated i.e.next()
closed
Generator has terminated by either encountering an error i.e.throw()
, returning i.e.return()
, or iterating through all the values i.e.yield
We can also make generator function nested by using yield*
which required a generator function as well. This is how you call a generator function from another :
function* delegate() {
yield 'Value 3'
yield 'Value 4'
}
function* begin() {
yield 'Value 1'
yield 'Value 2'
yield* delegate()
}
const generator = begin()
for (const value of generator) {
console.log(value)
}
// Output
// Value 1
// Value 2
// Value 3
// Value 4
Infinite data streams:
One of the most important features of generators is their ability to work with infinite data streams and collections. This means it’ll be a very useful scenario like if you’re implementing infinite scrolling etc. Let’s create a program to understand this.
We’ll write a program that will print Fibonacci numbers.
// Create a generator function
function* fibonacci() {
let prev = 0;
let next = 1;
yield prev
yield next
// Add previous and next values and yield them forever
while (true) {
const newVal = next + prev
yield newVal
prev = next
next = newVal
}
}
const fibGenerator = fibonacci()
// Print first five fib
for (let i = 0; i < 5; i++) {
console.log(fibGenerator.next())
}
console.log("===========")
//Print the next 5
for (let i = 0; i < 5; i++) {
console.log(fibGenerator.next())
}
// Output show in the screenshot
In a general scenario, it should print 0,1,1,2,3
again but instead, it starts from where it left because we didn’t use any return()
or throw()
.
Just like normal functions, we can also pass arguments to generator functions.
function* generatorFunction(value) {
while(true) {
value = yield value * 10
}
}
// Initiate a generator and seed it with an initial value
const generator = generatorFunction(0)
for (let i = 0; i < 5; i++) {
console.log(generator.next(i).value)
}
// Output
// 0
// 10
// 20
// 30
// 40
I hope this gives you a general idea of what generators are and you can always explore more: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*?retiredLocale=my