JavaScript Generator Functions
20 January, 2023
5
5
1
Contributors
In JavaScript, a generator function is a function that can be paused and resumed at a later time, allowing it to produce a sequence of values over time rather than computing them all at once and returning them in a single array.
Here is a simple example of a generator function that produces a sequence of numbers:
function* numbers() {
yield 1;
yield 2;
yield 3;
}
const generator = numbers();
console.log(generator.next().value); // 1
console.log(generator.next().value); // 2
console.log(generator.next().value); // 3
To create a generator function, you use the function*
syntax, and then use the yield
keyword to produce a value. Each time the generator is iterated (e.g., with a for
loop), it will execute the code up until the next yield
statement and then pause. When the generator is iterated again, it will pick up where it left off and continue executing until the next yield
statement.
Generator functions can be useful for producing large sequences of data, because they allow you to generate the values one at a time, rather than all at once. This can be more memory-efficient and can also allow you to work with the values as they are generated, rather than having to wait for the entire sequence to be computed before you can start processing it.
Here are a few examples of use cases for generator functions in JavaScript:
- Iterating over large datasets: If you have a large dataset that you want to process one item at a time, a generator function can be used to yield each item in the dataset as it is processed, rather than reading the entire dataset into memory all at once.
- Implementing infinite sequences: Generator functions can be used to create infinite sequences, such as an infinite range of numbers or an infinite stream of random numbers.
- Implementing asynchronous code: Generator functions can be used to write asynchronous code in a synchronous-looking style, using the
yield
keyword to pause the generator function and thenext()
method to resume it. This can make asynchronous code easier to read and write. - Implementing lazy evaluation: Generator functions can be used to implement lazy evaluation, where the next value in a sequence is not computed until it is needed. This can be useful for optimizing the performance of certain types of algorithms.
Here is an example of a generator function that produces an infinite sequence of random numbers:
function* randomNumbers() {
while (true) {
yield Math.random();
}
}
const generator = randomNumbers();
console.log(generator.next().value); // 0.123456789
console.log(generator.next().value); // 0.987654321
console.log(generator.next().value); // 0.654321987
Passing arguments to Generator functions
To pass arguments to a generator function in JavaScript, you can use the next()
method and pass the arguments as arguments to the next()
method.
Here is an example of a generator function that takes two arguments:
function* add(x, y) {
const result = x + y;
yield result;
}
const generator = add(10, 20);
console.log(generator.next().value); // 30
To pass different arguments to the generator function each time it is iterated, you can use a loop and call the next()
method with the desired arguments inside the loop.
Here is a tricky example of a generator function that takes two arguments and produces the results of adding them together for a range of input values:
function* add(x, y) {
while (true) {
const result = x + y;
x = yield result;
y = yield result;
}
}
const generator = add(10, 20);
for (let i = 1; i <= 5; i++) {
console.log(generator.next(i * 10, i * 20).value);
}
// Output:
// 30
// 30
// 50
// 50
// 90
In this example, the generator function has an infinite loop and uses the yield
keyword to pause the generator and return a value. The next()
method is called inside the loop with the new values for x
and y
as arguments, which causes the generator function to resume execution and compute the next result.
The above example is tricky because we have passed two parameters to the next()
method, which might confuse you. When it comes to the generation function and passing arguments, we need to understand a few things here:
- The generator function can be exited and later re-entered.
- The variable binding will be saved across all re-entrances.
- Calling a generator function doesn't execute the function immediately.
- The function's body is executed when we call the
next()
method until the execution hits theyield
. - The next() method takes only one parameter. All other parameters will be ignored.
- The value we pass using the next() method parameter will replace the
yield
expression where the execution was paused.
Now with all these understanding above, let's walk through the code.
At iteration 1
- x is 10, and y is 20
- It halts at x = yield result, and the result in the console is 30.
At iteration 2
- Here we call the next method, not the add() method again)
- Execution resumes
- x is 20 because the parameter is i * 10(i is 2)
- y remain the old value as 20(the second parameter of next method is ignored)
- it halts at y = yield result; and the the result in the console is 30 again(carrying the old summation)
At iteration 3
- Execution resumes
- x is 20 and continuing
- y is 30 and continuing
- It halts at x = yield result;
- so the result is 50. The console prints 50
At iteration 4
- Execution resumes
- now next() will replace the x value because it was halted x. So the new x value is
4*10
, which is 40. - y remain 30
- But no new result is computed. so print the old result 50
At iteration 5
- Execution resumes
- x remains 40, but now y will change to the value from next as 5 * 10, which is 50
- the new result expression executes and results in 90
Remember: In all these iterations, the second parameter of the next()
method doesn't have any role. its ignored.
We hope it helps.
Generators and Promises
In JavaScript, generator functions can be used in conjunction with Promises to handle asynchronous code in a synchronous-looking style.
Here is an example of using a generator function and a Promise to asynchronously fetch data from a remote API:
function* fetchData() {
const response = yield fetch('https://example.com/api/data');
const data = yield response.json();
return data;
}
function runGenerator(generator) {
const iterator = generator();
function iterate(iteration) {
if (iteration.done) return iteration.value;
const promise = iteration.value;
return promise.then(result => iterate(iterator.next(result)));
}
return iterate(iterator.next());
}
const dataPromise = runGenerator(fetchData);
dataPromise.then(data => {
console.log(data);
});
In this example, the fetchData
generator function uses the yield
keyword to pause execution and return a Promise representing the asynchronous action of fetching data from a remote API. The runGenerator
function is a helper function that takes a generator function and runs it, iterating through the generator and resolving the Promises returned by the generator function with the then()
method.
This allows you to write asynchronous code in a synchronous-looking style, using the yield
keyword to pause the generator function and the next()
method to resume it. This can make asynchronous code easier to read and write.