This content originally appeared on DEV Community and was authored by Pramit Marattha
JavaScript generators are one of the most misunderstood and complicated concepts in JavaScript because most people don't see the value in them, but bare in mind that this subject is actually quite useful. JavaScript generators first appeared in the ES6(ECMAScript2015) version of the JavaScript language.
A generator, in its most basic definition, is a process that can yield multiple different values and can be paused and resumed again. In JavaScript, a generator is made up of a generator function that produces an iterable Generator object. In addition, generators are able to handle unlimited data streams and can maintain state, which makes it possible to create iterators efficiently. Additionally, when used in conjunction with promises, generators can replicate the async/await capabilities, enabling us to interact with asynchronous code in a more direct and legible way. Although async/await is more frequently used to handle typical, straightforward concurrent use cases, such as getting data from an Application programming Interface (API) so on and so forth, generators offers more sophisticated features that make it simple to actually develop and utilize it.
Let's first look at the distinctions between generator functions and normal functions before moving on to this particular subject.
In this article, we'll learn everything there is to know about JavaScript generators, including how to create generator functions, iterate over Generator objects, differentiate between yield and return within a generator, and a variety of other features and aspects that generators offer. We'll also learn why and where to use generators, as well as why and where they are important.So without further ado, let's get started by first creating a very basic generators function.
// script.js
function* thisIsGenerator(){
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
}
The code above may have seemed a little strange to you and may have puzzled you because it is so peculiar and different from typical JavaScript. The asterisks(*)
symbol next to the Keyword function
may be the first unusual thing you notice. The asterisks(*)
are solely used to indicate to JavaScript that this is a generator function.
Note: This generator function name is just that — a function name, so you can give it whatever name you like. There are no rules or restrictions.
As you can see, there are five different yield
statements inside the generator function. We initially yield the first one, then second, and so on so forth. Yield is essentially similar to a special kind of return keyword that can only be used inside of a generator. This is because a generator's main goal is to execute some code, return a value, run more code, return another value, and so on until all the code inside of the generator has been successfully executed.
To use a generator, we must first run the generator function, which will give us an object called a generator object which we can use to manipulate the generator we just created. To do this, we must first obtain the generator object and execute the generator function, which will give us the generator object.
// script.js
function* thisIsGenerator(){
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
}
const genObj = thisIsGenerator()
console.log(genObj)
The code shown above will just log out and provide output that resembles something like this.
As you can see, it is a bit complicated/difficult to understand what is actually happening behind the scenes because so many different things are being shown to us, but if you expand the prototypes, you will notice that there are three main important functions: next
, return
, and throw
.
Next() Method
The next
method is the only one we are concerned with at the moment. In essence, this method enables us to run the code inside of our generator. So, for instance, if we call our genObj
with the next function, it will just return an object with value
set to 1
and done
set to false
.
// script.js
function* thisIsGenerator(){
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
}
const genObj = thisIsGenerator()
const nextGenObj = genObj.next()
console.log(nextGenObj) // {value:1,done:false}
It's crucial to keep in mind that every generator has two properties: a value and a done one. The done property will always be a Boolean value with the values true
or false
, where true
indicates that there is no more code to run and false
indicates that there is. Also keep in mind that the value
will always be whatever is yielded.
Note: The first time you run the generator, all it does is construct an object called the
generator object
, which you can then use to execute all of the code inside of it individually using itsnext()
property.
Therefore, when we executed genObj.next()
, all the code inside of our generator was executed up until the first yield, which in our instance was "1". Thereafter, the generator stopped/paused working.
// script.js
function* thisIsGenerator(){
// some code
yield 1;
// stops here
yield 2;
yield 3;
yield 4;
yield 5;
}
const genObj = thisIsGenerator()
console.log(genObj.next())
Now, if we were to run the next
method "three" times, observe that we would get three values up to 3
because it would start from the beginning of our function and yield 1
, pause until it reached the next statement, and then resume from where it left off to yield 2
, and then pauses once more before moving on to the next statement and finally yielding out 3
.
// script.js
function* thisIsGenerator(){
// some code
yield 1;
// pauses
// some code
yield 2;
// pauses
// some code
yield 3;
//pauses
yield 4;
yield 5;
}
const genObj = thisIsGenerator()
console.log(genObj.next())
console.log(genObj.next())
console.log(genObj.next())
Let's see what occurs when we run it a sixth time, just to be sure.
// script.js
function* thisIsGenerator(){
// some code
yield 1;
// pauses
// some code
yield 2;
// pauses
// some code
yield 3;
//pauses
// some code
yield 4;
// pauses
// some code
yield 5;
// pauses
}
const genObj = thisIsGenerator()
console.log(genObj.next())
console.log(genObj.next())
console.log(genObj.next())
console.log(genObj.next())
console.log(genObj.next())
console.log(genObj.next())
If we ran it a sixth time, as you can see, there would be no more code to run, so our value would be set to undefined
, and our done would be set to true
.
Executing Multiple Generators
One more thing to remember about generators is that they can all be running at the same time and simultaneously.
Assume we create two generator objects, both of which are based on our main generator. So, if we alternately executed our generator operator with our new generator object and executed it, you will notice that it prints out the initial value and then it starts back at the value 1
for our second generator object and the code continues alternately in the same pattern.
// script.js
function* thisIsGenerator(){
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
}
const genObj = thisIsGenerator()
const genSecondObj = thisIsGenerator()
console.log(genObj.next())
console.log(genSecondObj.next())
console.log(genObj.next())
console.log(genSecondObj.next())
console.log(genObj.next())
console.log(genSecondObj.next())
console.log(genObj.next())
console.log(genSecondObj.next())
console.log(genObj.next())
console.log(genSecondObj.next())
console.log(genObj.next())
console.log(genSecondObj.next())
This phenomenon occurs because whenever you call the generator object, you are essentially creating a brand new instance (that does not get reset) that is entirely separate and has its own version of this particular function that can iterate through things on its own and allows you to do the iteration on their own.
Where is Generators used?
One of the most frequent uses for generators is when you want to perform an infinite loop. In JavaScript, you can't perform an infinite loop because it will loop indefinitely and cause your program to freeze. However, by using a generator, you can perform an infinite loop that doesn't cause your PC to freeze to death because each step is only executed once.
If you need to, say, generate unique numbers and identifications, this capability is incredibly helpful. So let's create an infinite loop inside of our generator and try to go through each bits and pieces.
// script.js
function* infinite() {
let index = 1;
while (true) {
yield index
index++
}
}
const genObj = infinite();
console.log(genObj.next())
console.log(genObj.next())
console.log(genObj.next())
console.log(genObj.next())
console.log(genObj.next())
At first, we first created our index variable and gave it an initial value of 1 inside of our generator function, and after that, we created an infinite while loop. Inside of this infinite while loop, we yielded out our index and then we went on to increment it. So, voilà, we just made a generator that, when used repeatedly, would produce a unique number.
Return() Method
The return method in generators is really quite interesting since it allows you to essentially exit out of a generator no matter how much farther(on code) you have to go. It will also just return/spit it out whatever value you feed to it. Let's update our previous code and continue with the bit by bit explanation.
// script.js
function* infinite() {
let index = 1;
while (true) {
yield index
index++
}
}
const genObj = infinite();
console.log(genObj.next())
console.log(genObj.next())
console.log(genObj.return(100))
console.log(genObj.next())
console.log(genObj.next())
Let's examine the result of the above code by leaving everything else in the code unchanged and only passing return to it.
We will get 100 as an value that is because we are passing 100 to return and after that our done is set to true and undefined as being returned as our value that is because when we call return it is exiting out of our generator function as if it finished and its just going to return this value rightaway. So It immidiately exits our generator and does not allow us to have any more information in our generator at all this is a really great feature if you need to exit out of the generator prematurely you can just use this return() method and its going to exit out immideietly
When we use return, our generator function exits as if it has finished executing the entire code, and it will immediately return the value that we passed to it. So, in the code above we will receive "100" as a value because we passed "100" to return. The output following that return statement halted execution, giving us "done" set to "true" and "value" as the "undefined".
Therefore, if you need to exit the generator early, you can just call the return() method, and it will do so immediately. This is a pretty excellent feature because it prevents us from having any additional information in our generator at all.
Throw() method
Simply throwing an error is all that this function is used for. This function is quite handy if you plan to write some javascript libraries, but it's important to know that it exists in the wild. To use it, all you have to do is create a new instance of Error inside the throw function and pass the error message to it.
// script.js
function* infinite() {
let index = 1;
while (true) {
yield index
index++
}
}
const genObj = infinite();
console.log(genObj.next())
console.log(genObj.next())
console.log(genObj.throw(new Error("There is an error")))
console.log(genObj.next())
console.log(genObj.next())
Iterator
An iterator is an object which defines a sequence and potentially a return value upon its termination. Iterators also allow you to iterate over an object. Specifically, an iterator is any object which implements the Iterator protocol by having a next() method that returns an object with two properties:
- value: It represents the current value in the sequence.
- done: It represents whether the iteration is complete or not.
Iterator object can be deliberately iterated after creation by periodically executing next() method.
//script.js
function* thisIsGenerator(someArray){
for(let i = 0; i < someArray.length; i++){
yield someArray[i];
}
}
const genObj = thisIsGenerator(["First","Second","Third","Fourth"])
console.log(genObj.next())
console.log(genObj.next())
console.log(genObj.next())
console.log(genObj.next())
console.log(genObj.next())
So, if you wish to use an array in this iterator style syntax manner—which is not mandatorily required—the above-mentioned code is a really nice example of how to do so.
When building any type of JavaScript library, iterators come in quite helpful. If you are coming from another programming language, like Java, C++ & C#, you will be highly familiar with iterators because they are used frequently in those other languages.
The concept behind the generators is to enable you to quickly generate iterators without having to put in the time-consuming work in the background.
Conclusion
Finally, we learnt all there is to know about JavaScript generators, including the fundamentals and how to get started. To summarize what we learned in this article, we learned about what a generator in JavaScript is and why they are so helpful in our day-to-day life. Then we learned about the fundamentals of the generator, followed by some examples of its uses cases, and finally we proceeded towards the iterators and some advanced concepts about generators.
This content originally appeared on DEV Community and was authored by Pramit Marattha
Pramit Marattha | Sciencx (2022-07-04T09:07:00+00:00) JavaScript generators ELI5. Retrieved from https://www.scien.cx/2022/07/04/javascript-generators-eli5/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.