Understanding JavaScript Generators

Introduction

 
If you have been struggling with JavaScript iterators and searching about JavaScript generators, you are in the right place. In this article, we will take a look at JavaScript generators and how they are different from iterators. Moreover; we will go in-depth with its methods and see some tips when using function generators on an arrow function and inside an object.
 

Background

 
It’s good to have a basic understanding of iterators, one reason is to recognize the differences between iterators and generators. In such a case, you can read here or you can Google search. However; if this doesn’t apply to you, you can skip the reading part and proceed to the next section.
 

What is a JavaScript Generator?

 
Great! You have probably read the recommended reading material above before jumping into this section. And, hopefully, that helped and I want to say: “Thank you”. OK, then let’s get started.
 
In my opinion, JavaScript custom iterators are useful when utilized but it demands careful and thorough programming because of its internal state. Thereby, we have an alternative, JavaScript generator, to allow us (JavaScript programmers) to define a single function whose execution is continuous.
 
Things to remember about JavaScript Generators
  • Appears to be a normal function but behaves like an iterator.
  • It is written using the function* syntax.
  • Invoking a generator function doesn’t execute its body immediately, but rather returns a new instance of the generator object (an object that implements both, iterable and iterator protocols).
  • It is a function that returns multiple values one by one.
  • It can be paused and resumed. This is possible through the usage of the yield keyword.
Let's see an example below.
  1. /** 
  2.  * It is written using the function* syntax. [โœ“] 
  3.  * Appears to be a normal function. [โœ“] 
  4.  * Uses the yield keyword. [โœ“] 
  5.  */  
  6. function* myFavoriteFruits() {  
  7.    
  8.     let grapeFruit = yield "Grapefruit",  
  9.         pineApple = yield "Pineapple",  
  10.         apple = yield "Apples",  
  11.         mango = yield "Mango";  
  12. }  
Now, that we have seen how to declare a generator. Why not invoke the function generator? Finally, check and see the returned object.
 
Let’s see an example below. 
  1. /** 
  2.  * Invoking a generator function doesn't execute its body immediately.  
  3.  */  
  4. var mygenerator = myFavoriteFruits();  
  5.    
  6. //Output: Returns a new instance of the generator object.  
  7. console.log(mygenerator);  
Output
 
Understanding JavaScript Generators
 
As you can see, the myFavoriteFruits (generator function) returns an instance of the generator object. Moreover, this object implements both iterable and iterator protocols.
 
JavaScript Generator Instance Methods
 
The JavaScript generator has 3 instance methods,
  • next()
  • return()
  • throw()
The next() method
 
When the next() method of the generator object executes, it looks for the function’s body until the yield keyword is encountered. Thereby, it returns the yielded value and pauses the function. Again, when the next() method is invoked it resumes the execution, and then returns the next yielded value.
 
Lastly, the method next() has two properties: value and done.
  • value property is the returned value by the iterator.
  • done property is true when the generator function doesn’t yield any more value.
Let’s see an example below.
  1. function* myFavoriteFruits() {  
  2.    
  3.     let grapeFruit = yield "Grapefruit",  
  4.         pineApple = yield "Pineapple",  
  5.         apple = yield "Apples",  
  6.         mango = yield "Mango";  
  7.    
  8.     //output:undefined undefined undefined undefined -> more of this later.  
  9.     console.log(grapeFruit, pineApple, apple, mango);   
  10. }  
  11.    
  12. var mygenerator = myFavoriteFruits();  
  13.    
  14. //output: {value: "Grapefruit", done: false}  
  15. console.log(mygenerator.next());  
  16.    
  17. //output: {value: "Pineapple", done: false}  
  18. console.log(mygenerator.next());   
  19.    
  20. //output: {value: "Apples", done: false}  
  21. console.log(mygenerator.next());  
  22.    
  23. //output: {value: "Mango", done: false}  
  24. console.log(mygenerator.next());   
  25.    
  26. //output: {value: undefined, done: true}  
  27. console.log(mygenerator.next());   
Another thing to remember on the next() method is that we can pass an optional argument to it. This argument becomes the value returned by the yield statement, where the generator function is currently paused.
 
To go back to the previous example, on line 9 we have this statement: console.log(grapeFruit, pineApple, apple, mango); which resulted in undefined undefined undefined undefined. To fix this, let’s see an example below.
  1. function* myFavoriteFruits() {  
  2.    
  3.     let grapeFruit = yield "Grapefruit",  
  4.         pineApple = yield "Pineapple",  
  5.         apple = yield "Apples",  
  6.         mango = yield "Mango";  
  7.    
  8.     //output:Grapefruit Pineapple Apples Mango  
  9.     console.log(grapeFruit, pineApple, apple, mango);   
  10. }  
  11.    
  12. var mygenerator2 = myFavoriteFruits();  
  13.    
  14. //output: {value: "Grapefruit", done: false}  
  15. console.log(mygenerator2.next());   
  16.    
  17. //output: {value: "Pineapple", done: false}  
  18. console.log(mygenerator2.next("Grapefruit"));  
  19.    
  20. //output: {value: "Apples", done: false}  
  21. console.log(mygenerator2.next("Pineapple"));  
  22.     
  23. //output: {value: "Mango", done: false}  
  24. console.log(mygenerator2.next("Apples"));  
  25.     
  26. //output: {value: undefined, done: true}  
  27. console.log(mygenerator2.next("Mango"));   
The return() method
 
The return() method can end the generator function before it can yield all the values. Moreover, this method takes an optional argument and acts as the final value to return.
 
Let's see an example below. 
  1. function* beKindToPeople() {  
  2.        
  3.     yield "Be mindful";  
  4.     yield "Don't discriminate";  
  5.     yield "Buy someone a coffee";  
  6. }  
  7.    
  8. var myKindnessActGenerator = beKindToPeople();  
  9.    
  10. //output: Be mindful  
  11. console.log(myKindnessActGenerator.next().value);   
  12.    
  13. //output: Don't discriminate  
  14. console.log(myKindnessActGenerator.next().value);   
  15.    
  16. //output: {value: "Consider kindness before you speak", done: true}  
  17. console.log(myKindnessActGenerator.return("Consider kindness before you speak"));   
  18.    
  19. //output: true  
  20. console.log(myKindnessActGenerator.next().done);  
The throw() method
 
This method accepts the exception to be thrown. Thereby, you can manually trigger an exception inside the generator function.
 
Let us see an example below.
  1. function* howToBeHappy() {  
  2.        
  3.    
  4.     try {  
  5.         yield "Treat yourself like a friend";  
  6.     } catch (error) {  
  7.         console.log("Always be happy 1st try");  
  8.     }  
  9.    
  10.     try {  
  11.         yield "Challenge your negative thoughts";  
  12.     } catch (error) {  
  13.         console.log("Always be happy 2nd try");  
  14.     }  
  15.    
  16.     try {  
  17.         yield "Choose your friends wisely";  
  18.     } catch (error) {  
  19.         console.log("Always be happy 3rd try");  
  20.     }  
  21. }  
  22.    
  23. var myHappyGenerator = howToBeHappy();  
  24.    
  25. //output:  Treat yourself like a friend  
  26. console.log(myHappyGenerator.next().value);   
  27.    
  28. /** 
  29.  * output:  Always be happy 1st try 
  30.  *          Challenge your negative thoughts 
  31.  */  
  32. console.log(myHappyGenerator.throw("Exception thrown but still be happy").value);  
  33.    
  34. /** 
  35.  * output: Always be happy 2nd try 
  36.  *         Choose your friends wisely 
  37.  */  
  38. console.log(myHappyGenerator.throw("Exception thrown but still be happy").value);  
  39.    
  40. /** 
  41.  * output:Always be happy 3rd try 
  42.  *        true 
  43.  */  
  44. console.log(myHappyGenerator.throw("Exception thrown but still be happy").done);  
The “yield*” Keyword Inside The Function Generator
 
This expression is used to delegate to another generator or iterable object. Hence, it iterates it to yield its values.
 
Let’s see an example below.
  1. function* respectSubordinates() {  
  2.        
  3.     yield "Know their strengths and weaknesses";  
  4.     yield "Respectful to others."  
  5. }  
  6.    
  7. function* showLove() {  
  8.        
  9.     yield "Listen";  
  10.     yield* ['acts of service''encourage people''quality time'];  
  11.     yield* respectSubordinates();  
  12. }  
  13.    
  14. var loveIterator = showLove();  
  15.    
  16. for (const value of loveIterator) {  
  17.     console.log(value);  
  18. }  
Output
 
Understanding JavaScript Generators
 
Generator As Methods Inside An Object
 
Because JavaScript generator is a function too, you can add them to objects.
 
Let’s see some examples below.
  • Object literal with a function expression
    1. const fruits = {  
    2.     createFruitIterator: function* (items) {  
    3.         for (let index = 0; index < items.length; index++) {  
    4.             yield items[index];  
    5.         }  
    6.     }  
    7. }  
    8.    
    9. const myFruitIterator = fruits.createFruitIterator(  
    10.                                     ["Grapefruit",  
    11.                                     "Pineapple",  
    12.                                     "Apples",  
    13.                                     "Mango"]);  
    14.    
    15. for (const fruit of myFruitIterator) {  
    16.     console.log(fruit);  
    17. }  
  • Object literal using method shorthand by pretending the method name with an asterisk (*).
    1. const actOfKindNess = {  
    2.     *beKindToPeopleIterator(items) {  
    3.         for (let index = 0; index < items.length; index++) {  
    4.             yield items[index];  
    5.         }  
    6.     }  
    7. }  
    8.    
    9. const myKindnessActIterator = actOfKindNess.beKindToPeopleIterator(  
    10.                                             ["Don't discriminate",  
    11.                                              "Buy someone a coffee",  
    12.                                              "Consider kindness before you speak"]);  
    13.    
    14. for (const kindNess of myKindnessActIterator) {  
    15.     console.log(kindNess);  
    16. }  
The examples are almost the same as the previous examples but with different syntax.
 
Why I Can’t Use An Arrow Function with JavaScript Generators?
 
Based on my research it is because of the yield keyword. Please see the statement below from the MDN documentation:
Understanding JavaScript Generators 
“The yield keyword may not be used in an arrow function’s body unless it is further nested within it. Therefore, arrow functions cannot be used as generators.” 
 
Here are the things I experimented with arrow functions that failed.
 
Arrow function and using the yield keyword. This one failed.
 
Understanding JavaScript Generators
 
Arrow function and using the “yield*” keyword. This one failed too. 
 
Understanding JavaScript Generators
 
Arrow function and use the asterisk to act as a generator and removed the yield keyword. Again, this one failed. 
 
Understanding JavaScript Generators
 
Perhaps, the simple experiment is significant. However, come to think of it, I felt that using a generator in an arrow function appears vague.
 
Here is one of the things I observed with the arrow-function that was successful.
 
Before going to the sample code. Let us go back to the documentation. It says: “Unless it is further nested within it.” Let’s give it a try.
 
Let us see a code sample below. 
  1. /** 
  2.  * Arrow function that returns an object that has a function-generator. 
  3.  */  
  4. const mySampGenerator = () => ({  
  5.     *beKindToPeopleIterator(items) {  
  6.         for (let index = 0; index < items.length; index++) {  
  7.             yield items[index];  
  8.         }  
  9.     }  
  10. });  
  11. /** 
  12.  * Let's try if the function-generator works inside the arrow-function. 
  13.  */  
  14. let mySampIterator = mySampGenerator()  
  15.                         .beKindToPeopleIterator(  
  16.                             ["Don't discriminate",  
  17.                             "Buy someone a coffee",  
  18.                             "Consider kindness before you speak"]);  
  19.    
  20. console.log(mySampIterator);  
Output
 
Understanding JavaScript Generators
 
Difference Between Generators and Iterators
 
At last, this is our last section of this article. Based on research and observation we can conclude that an iterator traverses a collection one at a time while a generator generates a sequence, one item at a time.
 

Summary

 
In this post, we have tackled the concepts of JavaScript generators. We have started by looking at what a JavaScript generator is, go in-depth with its instance methods.
 
Moreover, we have seen why we can’t use an arrow function directly, and some tips concerning generator function inside an object. Lastly, we have seen the difference between an iterator and a generator.
 
I hope you have enjoyed this article, as I have enjoyed writing it. Stay tuned for more. This article was originally written and posted here. Many thanks, until next time, happy programming!