In this article, we will take a look at JavaScript generators and how it is 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.
Introduction
If you have been grasping or are done grasping JavaScript iterators and searching 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, 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 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:
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:
var mygenerator = myFavoriteFruits();
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 three instance methods:
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";
console.log(grapeFruit, pineApple, apple, mango);
}
var mygenerator = myFavoriteFruits();
console.log(mygenerator.next());
console.log(mygenerator.next());
console.log(mygenerator.next());
console.log(mygenerator.next());
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";
console.log(grapeFruit, pineApple, apple, mango);
}
var mygenerator2 = myFavoriteFruits();
console.log(mygenerator2.next());
console.log(mygenerator2.next("Grapefruit"));
console.log(mygenerator2.next("Pineapple"));
console.log(mygenerator2.next("Apples"));
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.
function* beKindToPeople() {
yield "Be mindful";
yield "Don't discriminate";
yield "Buy someone a coffee";
}
var myKindnessActGenerator = beKindToPeople();
console.log(myKindnessActGenerator.next().value);
console.log(myKindnessActGenerator.next().value);
console.log(myKindnessActGenerator.return("Consider kindness before you speak"));
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();
console.log(myHappyGenerator.next().value);
console.log(myHappyGenerator.throw("Exception thrown but still be happy").value);
console.log(myHappyGenerator.throw("Exception thrown but still be happy").value);
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 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 equivalent to the previous examples; they just use a 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:
const mySampGenerator = () => ({
*beKindToPeopleIterator(items) {
for (let index = 0; index < items.length; index++) {
yield items[index];
}
}
});
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 leave a comment below. Many thanks, until next time, happy programming!