Generator Functions in JS
chamindu lakshan
Out of the box thinker/YouTubepreneuer/programmer/Wordpress and Wix Designer
What are Generator Functions?
Generator functions are a special type of functions in JavaScript, introduced in ES6, that have the built-in capability to be paused and resumed allowing us to take control of the execution flow and generate multiple values.
The syntax, as shown below, is pretty much similar to regular functions apart from the new function* keyword.
function* someGeneratorFunction() {
// function code goes here
}
But what does it mean to “pause and resume” execution?
To understand this better, let’s look at how regular functions behave.
Regular functions execute from start to finish sequentially. Once a function is invoked, it continues execution until it encounters either a return statement or reaches the end of the function body.
A basic demonstration:
function someRegularFunction() {
console.log("1");
console.log("2");
console.log("3");
}
someRegularFunction();
// Output will be as follows:
// 1
// 2
// 3
In the snippet above, when the function is called, it executes each line linearly (by the order they were defined) and it is reflected in the output.
Generator Functions on the other hand can use the yield keyword to pause execution and generate a value. When this value is yielded, the state of the function is saved and can be resumed at a later time, by calling the next() method, from where it left off.
This means that we can yield multiple values by exiting and re-entering the function.
A basic demonstration:
Note: Outputs of the calls are commented below each line
function* someGeneratorFunction() {
console.log("Start of the function");
yield 1;
console.log("Middle of the function");
yield 2;
console.log("End of the function");
}
const generator = someGeneratorFunction(); // Returns generator object
console.log(generator.next().value);
// Start of the function
// 1
console.log(generator.next().value);
// Middle of the function
// 2
console.log(generator.next().value);
// End of the function
// undefined
In the snippet above, calling the generator function someGeneratorFunction* returns a generator object. On this object, we can call the next() method, which will cause the generator function to execute. If a yield is encountered, the method returns an object that contains a value property containing the yielded value and a boolean done property which indicates if the generator has yielded its last value or not. To demonstrate this, we will log the entire object returned from next() (as opposed to the value property as we did in the previous example).
function* someGeneratorFunction() {
yield 1;
}
const generator = someGeneratorFunction();
console.log(generator.next()); // {value: 1, done: false}
console.log(generator.next()); // {value: undefined, done: true}
What happens if we add a return statement in a generator function?
Much like how it behaves in regular functions, a return statement will cause the generator function to finish executing, making the subsequent lines of codes unreachable. It does this by setting the done property to true.
function* yieldAndReturn() {
yield 1;
return "Returned";
yield "Unreachable";
}
const generator = yieldAndReturn();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: "Returned", done: true }
console.log(generator.next()); // { value: undefined, done: true }
Note: In a real world implementation, we wouldn’t know how many times to request for the values, so we would need to keep requesting values in a loop until we receive the done: true property.
Why and When do we use Generator Functions?
You might have been wondering — couldn’t this “pause and resume” functionality be achieved using regular functions?
Well, sure it can:
Here is a painful example:
领英推荐
asyncFunction1(arg1, (err, result1) => {
if (err) {
console.error(err);
} else {
asyncFunction2(result1, (err, result2) => {
if (err) {
console.error(err);
} else {
asyncFunction3(result2, (err, result3) => {
if (err) {
console.error(err);
} else {
// Do something with the final result
}
});
}
});
}
});
asyncFunction1(arg1)
.then(result1 => asyncFunction2(result1))
.then(result2 => asyncFunction3(result2))
.then(result3 => {
// Do something with the final result
})
.catch(err => {
console.error(err);
});
Why use Generator Functions?
When to use Generator Functions:
Let’s see a few scenarios where using a generator function might come in handy.
function* fibonacciSequence() {
let a = 0;
let b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const fibonacciGenerator = fibonacciSequence();
console.log(fibonacciGenerator.next().value); // Output: 0
console.log(fibonacciGenerator.next().value); // Output: 1
console.log(fibonacciGenerator.next().value); // Output: 1
console.log(fibonacciGenerator.next().value); // Output: 2
2. Asynchronous Programming
Generator functions handle asynchronous code with ease by avoiding complex promise chains. They are the foundations on which async/await syntax is built.
Since yield stops the execution of the function, we can use it to wait for an asynchronous request to resolve.
Solving the previous callback hell
Remember our previous callback example where we encountered the callback hell? Recalling that we needed to make multiple API calls where the current call depends on the result of the previous one, here is how generator functions solve it:
function* asyncFunction1(arg1) {
try {
const result1 = yield async_function1(arg1);
const result2 = yield async_function2(result1);
const result3 = yield async_function3(result2);
// Do something with the final result
} catch (err) {
console.error(err);
}
}
function run(generator) {
const iterator = generator(); // Create the generator object
function iterate({ value, done }) { // Destructure the value & done properties
if (done) {
return;
}
value
.then((result) => iterate(iterator.next(result))) // Call the next iteration and pass the current result
.catch((err) => iterate(iterator.throw(err)));
}
iterate(iterator.next()); // Start the iteration
}
run(asyncFunction1);
This code can easily be rewritten using async/await — Even better!
Note that async/await uses generator functions behind the scenes.
async function asyncFunction1(arg1) {
try {
const result1 = await asyncFunction1(arg1);
const result2 = await asyncFunction2(result1);
const result3 = await asyncFunction3(result2);
// Do something with the final result
} catch (err) {
console.error(err);
}
}
asyncFunction1();
Conclusion
As we have seen, generator functions allow you to define functions that can be paused and resumed, enabling the creation of iterators and asynchronous programming constructs like async/await. They provide an easy-to-understand syntax, as well as an optimized method of handling iterations for yielding multiple values on demand. These features make them particularly useful in scenarios that involve working with sequences, implementing iterators, and writing asynchronous code.
(source = "internet")