Generators are special kind of functions which yields a set of values and returns an iterator which can traverse through the set of values via next()
function.
Functions who have got *
after function
keyword are generator functions.
function* simpleGenerator() {
yield 1;
yield 2;
yield 3;
}
Let’s take the above generator function as an example. What if we do -
const c = simpleGenerator();
console.log(c.next());
The console.log will print out an object -
{ value: 1, done: false }
What if we do the same console.log(c.next())
again. We’ll see -
{ value: 2, done: false }
One more time, we’ll get -
{ value: 3, done: false }
and now, once all the values have been yielded, if we do a console.log(c.next())
again, we’ll get -
{ value: undefined, done: true }
As you can see, generator functions have the ability to pause their execution till next yield. And it returns a done
flag which keeps the user informed if all the values have been yielded or not.
Multiple Generator Objects
Let’s say we create multiple generator objects by calling generator function multiple times -
const c1 = simpleGenerator();
const c2 = simpleGenerator();
A new generator function call will generate a new fresh instance of the iterator which can be used to get all the yielded values again.
So, if we have a code like -
// first run
console.log(c1.next())
console.log(c2.next())
// second run
console.log(c1.next())
console.log(c2.next())
We’ll get this in the console -
{ value: 1, done: false }
{ value: 1, done: false }
{ value: 2, done: false }
{ value: 2, done: false }
Once, a generator instance has been exhausted, it can’t be used again.
Return values of yield
Whenever a value is yielded inside a generator function, it also returns a value back which is nothing but whatever we passed to the next()
function.
One more important rule to consider - value passed to the first next()
function, won’t make any difference.
In the first iteration, yield will always return null.
console.log(c1.next(23432)) // won't matter
console.log(c1.next(2)) // will be returned
console.log(c1.next(3)) // will be returned
return()
function
return()
function is used to exit a generator function prematurely. A value can also be passed to return which will just set the returned value
to be that passed value.
So, if I do console.log(c1.return(3000))
, it will output -
{ value: 3000, done: true }
throw()
function
As name suggests, it is used to just throw any Error in between.
c1.throw(new Error("Something went wrong"));
Applications of Generators
Infinity loop
As we all know, we can’t have infinity loops in our code, it will force our application to crash. But, what if we have a function which can infinitely generate new values ?
As an example, a function who will return a unique id every time can be achieved via Generators -
function* generateID(prefix = "id-") {
let i = 0;
while(true) {
yield `${prefix}${i++}`;
}
}
const idGenerator = generateID();
Now, every time we call idGenerator.next()
, a new ID will be generated, and this will always return a unique value.
If we want to again start the id counter from 0
, we can create a new instance of generateID
generator function.
Creating Iterators
An iterator is a special entity in JavaScript which has got a next()
, return()
and a throw()
function, precisely what we get after calling the generator function.
Generators can be used to convert normal arrays into Iterators -
function* arrToIterator(arr = []) {
for (let i = 0; i < arr.length; i++) {
yield arr[i];
}
}
const i1 = arrToIterator([1, 3, 5]);
console.log(i1.next()); // will print { value: 1, done: false }
console.log(i1.next()); // will print { value: 3, done: false }