In this section, we will learn what the Generator Functions are and how to use them in JavaScript.
Note: we’re assuming you’re already familiar with the iterables and iterators in JavaScript.
What is Generator Function in JavaScript?
A generator function is another way of creating an iterator to iterate through the elements of a non-iterable object.
How to Declare a Generator Function in JavaScript? (Generator Function Syntax)
At its core, a generator function has pretty much the same syntax as a typical function. Except for generator functions, we put an asterisk `*` in between the keyword `function` and the name of that function.
Example:
function * generatorName(){…}
Note: anywhere that we can use functions, we can use generators as well.
For example:
// Generator function declaration function* generatorFn() {} // Generator function expression let generatorFn = function* () {} // Object literal method generator function let foo = { * generatorFn() {} } // Class instance method generator function class Foo { * generatorFn() {} } // Class instance method generator function class Foo { static * generatorFn() {} }
Note: WE CANNOT USE ARROW FUNCTIONS TO CREATE GENERATORS.
When we call a generator function, it will return an iterator object. This object has the same structure that we saw in the iterator section. This means the iterator has one method named `next()` and if we call it, it will return an object with two properties:
- `value`: which contains the value.
- `done`: defines whether there’s another element (value) to be returned if we call the `next()` method.
Note: read the Iterator section if you’re not familiar with the `Iterator` protocol.
Generator yield
Now let’s see how we define the values in the generator functions.
To define elements in the generator functions, we use the `yield` statement. On the right side of this keyword, we put the value that we want to be returned when we call the `next()` method of the iterator object.
Example: declaring a generator function in JavaScript
function * gen(){ yield 10; yield 11; yield 12; } const itr = gen(); console.log(itr.next()); console.log(itr.next()); console.log(itr.next()); console.log(itr.next());
Output:
{value: 10, done: false} {value: 11, done: false} {value: 12, done: false} {value: undefined, done: true}
Invoking JavaScript Generator Function
Invoking a generator function means calling that function in order to get its iterator object.
In the body of the generator function `gen()` of the last example, we defined 3 `yield` statements to return the values 10, 11, and 12.
So when we called the generator `gen()` in the statement below:
const itr = gen();
The `iterator object` of the generator `gen()` is passed to the identifier `itr`.
JavaScript Generator next() Method
Now in the last example, every time we call the `next()` method via the identifier `itr`:
- The execution engine jumps to the body of the target generator function to access the next `yield` statement. After reaching to the `yield` statement, it returns the object with the `value` property contains the value on the right side of the `yield` keyword and the `done` property set to `false` (or `true` if there’s no other yield statement). This returned object is basically considered as the returned value of the `next()` method.
Note: the first time we call the `next()` method, it will reach to the first `yield` statement.
In this program, we’ve called the `next()` method 4 times.
The first call to the `next()` method returned the object that contained the `value` property set to `10` and the `done` property set to `false`. (The value belongs to the first `yield` statement)
The second call to the `next()` method returned the object that contained the `value` property set to `11` and the `done` property set to `false`. (The value belongs to the second `yield` statement)
The third call to the `next()` method returned the object that contained the `value` property set to `12` and the `done` property set to `false`. (The value belongs to the third `yield` statement)
The last call to the `next()` method returned the object that contained the `value` property set to `undefined` and the `done` property set to `true`.
Basically, for the last call, because there was no other `yield` statement, the value property became `undefined` and the `done` property’s value turned into `true` which signals the end of the elements in the target generator function.
From this moment onwards, no-matter how many times we call the `next()` method, it will return the same object. (The `value` property set to `undefined` and the `done` property to `true`).
Note: We can also pass an argument to the `next()` method and that will be passed to the body of the target generator function where the related `yield` statement is. Basically, the value that we pass as the argument will be replaced with the next `yield` statement. (Note that still the value of the `yield` statement will return as the result of calling the `next()` method). If you think about it, this is a dual communication.
Example: invoking a generator function in JavaScript
function * gen(){ console.log(`Inside the generator function 'gen': ${yield 10}`); console.log(`Inside the generator function 'gen': ${yield 11}`); console.log(`Inside the generator function 'gen': ${yield 12}`); } const iterator = gen(); console.log(iterator.next("John")); console.log(iterator.next("Doe")); console.log(iterator.next("Omid")); console.log(iterator.next("Dehghan"));
Output:
{value: 10, done: false} Inside the generator function 'gen': Doe {value: 11, done: false} Inside the generator function 'gen': Omid {value: 12, done: false} Inside the generator function 'gen': Dehghan {value: undefined, done: true}
One thing that you should know about putting an argument in the `next()` method is that the first time we put an argument into this method, it won’t be used as the replacement of the first `yield` statement.
This is because the first call to the `next()` method just starts the iterator and causes the execution engine to move from the top of the target generator function to the first `yield` statement. So we get the value of the first `yield` statement, but we can’t send a value to it with the first call to the `next()` method.
That’s how the first argument to the `next()` method in the program above (which was “John”) was simply ignored.
Nonetheless, the second time we call the next() method with a value, that value will replace the first `yield` statement in the target generator function. So if you need a value for the first yield statement, put that value on the second call of the next() method instead of the first call!
JavaScript Generator Functions and for of loop
Considering the fact that the generator functions are also iterable, we can use the JavaScript’s constructs, like the `for of` loop, to iterate through the elements of a generator function.
Example: generator functions and for of loop in JavaScript
function * gen(){ yield 10; yield 11; yield 12; } for (const value of gen()){ console.log(value); }
Output:
10 11 12
JavaScript Iterable object as the value of yield statement
Note that inside the body of a generator function we can put other iterable objects as the value of the `yield` statement as well.
Now, if the value of a `yield` statement is an iterable and we want to iterate through those elements as well, we need to put another asterisk `*` after the keyword `yield`.
Example: using another iterable object as the value of yield statement in JavaScript
const obj = { [Symbol.iterator](){ let counter = 0; return { next(){ counter++; if (counter ==1){ return {value: 12, done:false}; } if(counter ==2){ return {value: 13, done:false}; } if (counter ==3){ return {value: 14, done:false}; } return {value: undefined, done:true}; } } } } function * gen(){ yield 10; yield 11; yield * obj; yield 15; } for (const value of gen()){ console.log(value); }
Output:
10 11 12 13 14 15
In the body of the `gen()`, the first two `yield` statements are the standard ones. But then in the third statement:
yield * obj;
We’ve passed an iterable object. So here the execution engine will move on to the iterator object of `obj` and return the elements of that object.
After all the elements of this object is returned, the execution engine will move back to the body of the `gen()` and return the value of the last yield statement, which is `yield 15;`.
Note: again, if you’re not familiar with the iterables and iterators, please refer to the related section.
JavaScript Wrong ways of using the yield statement
Be aware that the `yield` keyword can only be used in the body of a generator function.
Here is the list of invalid use of the yield statement:
// invalid function* invalidGeneratorFnA() { function a() { yield; } } // invalid function* invalidGeneratorFnB() { const b = () => { yield; } } // invalid function* invalidGeneratorFnC() { (() => { yield; })(); }
JavaScript Generator return() Method
We mentioned that as long as there’s a value (the `yield` statement) in the body of a generator, the `done` property of the returned object will be set to `false`.
But if we want to terminate the iteration early, we can call the `return()` method. After calling this method, if we attempt to call the `next()` method again, the `value` property of the returned object will be set to `undefined` and the `done` property to `true`.
Example: using generator return() method in JavaScript
function * gen(){ yield 10; yield 11; yield 12; yield 13; yield 14; yield 15; } const iterator = gen(); console.log(iterator.next()); console.log(iterator.next()); iterator.return(); console.log(iterator.next());
Output:
{value: 10, done: false} {value: 11, done: false} {value: undefined, done: true}
As you can see, the first 2 calls to the `next()` method, returned two objects with the value 10 and 11. But after that, in the statement:
iterator.return();
This statement terminates the iterator object that we got from the `gen()`. This means if we call the `next()` method after this statement, we will get an object with the value property set to `undefined` and the done property to true.
JavaScript Generator throw() Method
There’s another method named `throw()` that also can be used to terminate the target `iterator` object. This method is mainly used when there’s an error in the program and we want to terminate the iteration because of this error.
To be more specific, the `throw()` method takes one argument (that is the error message) and returns that to the body of the generator function.
Now, if in the body of the generator this error is handled correctly via the `try catch` blocks, then the generator can proceed to generate other values. But if there’s no handling mechanism available in the target generator function, the iterator will close.
Note: in the `try catch` block section, we will explain in details how to handle errors.
Example: using JavaScript Generator throw() method
function * gen(){ yield 10; yield 11; yield 12; yield 13; yield 14; yield 15; } const iterator = gen(); console.log(iterator.next()); console.log(iterator.next()); iterator.throw("Error"); console.log(iterator.next());
Output:
{value: 10, done: false} {value: 11, done: false} Uncaught Error
In the body of the generator function `gen()` we didn’t handle the error in any way, so the iteration closed.
Note: there are more into the way errors spread and cause the termination of a program. We covered this matter in the try-catch blocks section.