Understanding JavaScript Generators

Introduction

If you have been grasping or done grasping on JavaScript iterators and searching on JavaScript generators, you are at the right place. In this article, we will take a look at JavaScript generators and how they are different from iterators. Moreover; 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 suggest 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

  • It 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.

/**
 * It is written using the function* syntax. [✓]
 * Appears to be a normal function. [✓]
 * Uses the yield keyword. [✓]
 */
function* myFavoriteFruits() {

    let grapeFruit = yield "Grapefruit",
        pineApple = yield "Pineapple",
        apple = yield "Apples",
        mango = yield "Mango";
}

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.

/**
 * Invoking a generator function doesn't execute its body immediately. 
 */
var mygenerator = myFavoriteFruits();

//Output: Returns a new instance of the generator object.
console.log(mygenerator);

Output

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() and 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.

function* myFavoriteFruits() {

    let grapeFruit = yield "Grapefruit",
        pineApple = yield "Pineapple",
        apple = yield "Apples",
        mango = yield "Mango";

    //output:undefined undefined undefined undefined -> more of this later.
    console.log(grapeFruit, pineApple, apple, mango); 
}

var mygenerator = myFavoriteFruits();

//output: {value: "Grapefruit", done: false}
console.log(mygenerator.next());

//output: {value: "Pineapple", done: false}
console.log(mygenerator.next()); 

//output: {value: "Apples", done: false}
console.log(mygenerator.next());

//output: {value: "Mango", done: false}
console.log(mygenerator.next()); 

//output: {value: undefined, done: true}
console.log(mygenerator.next()); 

Another thing to remember on the next() method. 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.

function* myFavoriteFruits() {

    let grapeFruit = yield "Grapefruit",
        pineApple = yield "Pineapple",
        apple = yield "Apples",
        mango = yield "Mango";

    //output:Grapefruit Pineapple Apples Mango
    console.log(grapeFruit, pineApple, apple, mango); 
}

var mygenerator2 = myFavoriteFruits();

//output: {value: "Grapefruit", done: false}
console.log(mygenerator2.next()); 

//output: {value: "Pineapple", done: false}
console.log(mygenerator2.next("Grapefruit"));

//output: {value: "Apples", done: false}
console.log(mygenerator2.next("Pineapple"));
 
//output: {value: "Mango", done: false}
console.log(mygenerator2.next("Apples"));
 
//output: {value: undefined, done: true}
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.

function* beKindToPeople() {
    
    yield "Be mindful";
    yield "Don't discriminate";
    yield "Buy someone a coffee";
}

var myKindnessActGenerator = beKindToPeople();

//output: Be mindful
console.log(myKindnessActGenerator.next().value); 

//output: Don't discriminate
console.log(myKindnessActGenerator.next().value); 

//output: {value: "Consider kindness before you speak", done: true}
console.log(myKindnessActGenerator.return("Consider kindness before you speak")); 

//output: true
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.

function* howToBeHappy() {
    

    try {
        yield "Treat yourself like a friend";
    } catch (error) {
        console.log("Always be happy 1st try");
    }

    try {
        yield "Challenge your negative thoughts";
    } catch (error) {
        console.log("Always be happy 2nd try");
    }

    try {
        yield "Choose your friends wisely";
    } catch (error) {
        console.log("Always be happy 3rd try");
    }
}

var myHappyGenerator = howToBeHappy();

//output:  Treat yourself like a friend
console.log(myHappyGenerator.next().value); 

/**
 * output:  Always be happy 1st try
 *          Challenge your negative thoughts
 */
console.log(myHappyGenerator.throw("Exception thrown but still be happy").value);

/**
 * output: Always be happy 2nd try
 *         Choose your friends wisely
 */
console.log(myHappyGenerator.throw("Exception thrown but still be happy").value);

/**
 * output:Always be happy 3rd try
 *        true
 */
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.

function* respectSubordinates() {
    
    yield "Know their strengths and weaknesses";
    yield "Respectful to others."
}

function* showLove() {
    
    yield "Listen";
    yield* ['acts of service', 'encourage people', 'quality time'];
    yield* respectSubordinates();
}

var loveIterator = showLove();

for (const value of loveIterator) {
    console.log(value);
}

Output

Generator As Methods Inside An Object

Because JavaScript generator is a function too, you can add them to objects, too.

Let’s see some examples below.

  • Object literal with a function expression.
const fruits = {
    createFruitIterator: function* (items) {
        for (let index = 0; index < items.length; index++) {
            yield items[index];
        }
    }
}

const myFruitIterator = fruits.createFruitIterator(
                                    ["Grapefruit",
                                    "Pineapple",
                                    "Apples",
                                    "Mango"]);

for (const fruit of myFruitIterator) {
    console.log(fruit);
}
  • Object literal using method shorthand by prepending the method name with an asterisk (*).
const actOfKindNess = {
    *beKindToPeopleIterator(items) {
        for (let index = 0; index < items.length; index++) {
            yield items[index];
        }
    }
}

const myKindnessActIterator = actOfKindNess.beKindToPeopleIterator(
                                            ["Don't discriminate",
                                             "Buy someone a coffee",
                                             "Consider kindness before you speak"]);

for (const kindNess of myKindnessActIterator) {
    console.log(kindNess);
}

These 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:

“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.

Arrow function and using the “yield*” keyword. This one failed too.

Arrow function and use the asterisk to act as a generator and removed the yield keyword. Again, this one failed.

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.

/**
 * Arrow function that returns an object that has a function-generator.
 */
const mySampGenerator = () => ({
    *beKindToPeopleIterator(items) {
        for (let index = 0; index < items.length; index++) {
            yield items[index];
        }
    }
});
/**
 * Let's try if the function-generator works inside the arrow-function.
 */
let mySampIterator = mySampGenerator()
                        .beKindToPeopleIterator(
                            ["Don't discriminate",
                            "Buy someone a coffee",
                            "Consider kindness before you speak"]);

console.log(mySampIterator);

Output

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. If you feel that you have something to say, please comment on the comment section below because this will encourage us to grow towards our journey learning the JavaScript language. Don’t forget to subscribe, if you haven’t subscribed yet. Many thanks, until next time, happy programming!