Introduction
The Generator function (function*
) is a powerful feature in JavaScript that allows you to iterate over a sequence of values lazily, meaning they produce values only when needed.
Generator functions behave differently from regular functions in javascript. While a regular function returns a value on execution, a generator function can return multiple values, one after the other, on-demand. When a generator function is called, it does not runs its code and instead, it returns a special object call generator object called generator
, to manage the execution.
Syntax
function* name(param1) {
statements;
}
Parameters
name: The function name.
param1: The name of a formal parameter for the function. It is optional.
Statements: The body of the function. It is optional.
Generator Methods
The primary method is next()
, which is used to retrieve the next value yielded by the generator. The next() method of a generator function returns an object with two properties: value and done. While value
property represents the value yielded by the generator function. If the generator function contains a yield statement, the value property will be set to the value specified in the yield statement. If the generator function has finished executing or there are no more values to yield, the value property will be undefined, the done
property indicates whether the generator function has finished executing (true) or not (false). If the generator function has finished executing, done will be true. If there are more values to yield, done will be false. See example:
function* generateFunc() {
yield 1;
yield 2;
return 3;
}
const generator = generateFunc();
const firstValue = generator.next();
console.log(JSON.stringify(firstValue)); // output: {"value":1,"done":false}
let secondValue = generator.next();
console.log(JSON.stringify(secondValue)); // output: {"value":1,"done":false}
However, there are a couple of other methods associated with generator objects:
return()
The return()
method is used to forcefully terminate the generator. It can be called with an optional argument, which becomes the value of the yield expression that caused the generator to finish.
Example:
function* myGenerator() {
yield 1;
yield 2;
yield 3;
yeild 5
}
const generator = myGenerator();
console.log(generator.next()); // Output: { value: 1, done: false }
console.log(generator.return(4)); // Output: { value: 4, done: true }
console.log(generator.next()); // Output: { value: undefined, done: true }
throw()
The throw() method is used to throw an exception into the generator. It can be caught within the generator using a try…catch block.
Example:
function* myGenerator() {
try {
yield 1;
yield 2;
} catch (error) {
console.log('Error caught:', error);
}
}
const generator = myGenerator();
console.log(generator.next()); // Output: { value: 1, done: false }
console.log(generator.throw('Error occurred')); // Output: Error caught: Error occurred
console.log(generator.next()); // Output: { value: undefined, done: true }
Generators are iterables
There is different between a generator and a generator function. A generator is the response returned from the execution of a generator function. Generators are iterable because you can loop over the values using for..of
loop. See example below:
function* myGenerator() {
yield 1;
yield 2;
yield 3;
}
const generator = myGenerator();
for (let value of generator) {
console.log;
}
Optionally, a generator can be looped over by tranforming it to an array and then use array method to loop over it as below:
Using spread operator (...
)
You can use the spread operator to expand the generated values of a generator function into an array or another iterable object.
Example:
function* myGenerator() {
yield 1;
yield 2;
yield 3;
}
const generator = myGenerator();
const values = [...generator]; // Spread the generated values into an array
console.log(values); // Output: [1, 2, 3]
Using Array.from()
You can use the Array.from()
method to create an array from the generated values of a generator function.
Example:
function* myGenerator() {
yield 1;
yield 2;
yield 3;
}
const generator = myGenerator();
const values = Array.from(generator); // Create an array from the generated values
console.log(values); // Output: [1, 2, 3]
Generator Composition
Generator composition refers to the practice of combining multiple generator functions to create new generator functions. This allows you to modularize and reuse generator logic, similar to how functions are composed in functional programming.
Generator composition is achieved by using yieldstatements within a generator function. The yield statement delegates the execution of another generator function from within the current generator function, effectively chaining the two generators together.
Here is an example
function* generator1() {
yield 'a';
yield 'b';
}
function* generator2() {
yield '1';
yield '2';
}
function* composedGenerator() {
yield* generator1();
yield* generator2();
}
const generator = composedGenerator();
console.log(generator.next()); // Output: { value: 'a', done: false }
console.log(generator.next()); // Output: { value: 'b', done: false }
console.log(generator.next()); // Output: { value: '1', done: false }
console.log(generator.next()); // Output: { value: '2', done: false }
console.log(generator.next()); // Output: { value: undefined, done: true }
In the example above, composedGenerator
is a new generator function that combines the logic of generator1
and generator2
using yield*
statements. When composedGenerator is executed, it yields values from generator1
until it is exhausted, then it yields values from generator2
. This demonstrates how generator composition can be used to create more complex generator functions by reusing existing generator logic.
Practical Example
Generator can be handy when performance is key. Here are examples demonstrating how generator function can be used to improve the performance of your logics:
Fetching and paginate data from the server
async function fetchData(url, page, pageSize) {
const response = await fetch(`${url}?_page=${page}&_limit=${pageSize}`);
if (!response.ok) {
throw new Error(`Failed to fetch data: ${response.statusText}`);
}
const data = await response.json();
return data;
}
async function* paginateData(url, pageSize) {
let currentPage = 1;
while (true) {
const data = await fetchData(url, currentPage, pageSize);
if (data.length === 0) {
break; // No more data
}
yield { data, currentPage };
currentPage++;
}
}
// Wrapper to handle pagination
(async () => {
const url = 'https://jsonplaceholder.typicode.com/posts'; // Example API endpoint
const pageSize = 10; // Number of items per page
let currentPage = 1;
let dataGen = paginateData(url, pageSize);
try {
// Function to get the next set of data
async function getNextPage() {
const result = await dataGen.next();
if (!result.done) {
currentPage = result.value.currentPage;
return result.value.data;
} else {
return null; // No more data
}
}
// Function to get the previous set of data
async function getPreviousPage() {
// Todo: complete the logic to fetch the previous list of data
console.log('This is intended. You should complete it.');
}
console.log('First batch:', await getNextPage()); // return the first batch
console.log('Next batch:', await getNextPage()); // return the next batch
console.log('Previous batch:', await getPreviousPage()); // return the previous batch
} catch (error) {
console.error('Error fetching data:', error);
}
})();
In the example above, the API endpoint used is https://jsonplaceholder.typicode.com/posts, which is a placeholder API that provides fake posts for testing and prototyping. The above code fetch data from the api, paginate and lazy load them.
Generating an infinite sequence of values.
// 1. Generator function to produce unique order IDs
function* orderIdGenerator() {
let currentId = 1;
while (true) {
yield `ORDER-${currentId.toString().padStart(6, '0')}`;
currentId++;
}
}
// 2. Simulating an order processing system
const orderGen = orderIdGenerator();
function processOrder(customerName, orderDetails) {
const orderId = ''; // TODO: using the orderIdGenerator, generate the order id
const order = {
id: orderId,
customerName: customerName,
orderDetails: orderDetails,
timestamp: new Date().toISOString()
};
// Simulate order processing (e.g., saving to database, sending confirmation)
console.log('Processing Order:', order);
}
// Usage
processOrder('Alice', { item: 'Laptop', quantity: 1 });
processOrder('Bob', { item: 'Smartphone', quantity: 2 });
processOrder('Charlie', { item: 'Book', quantity: 3 });
// Continue processing orders as needed
In the example above, the orderIdGenerator generates an infinite sequence of values and assign to variable orderId.