DEV Community

francesco agati
francesco agati

Posted on

Generators in JavaScript

Generators in JavaScript are a powerful feature introduced in ECMAScript 6 (ES6) that allow you to define iterative algorithms by pausing and resuming execution at defined points. They provide a flexible way to control iteration and asynchronous operations.

How Generators Work

Generators are defined using function* syntax (note the asterisk). They use the yield keyword to pause execution and return values sequentially. When called, a generator function returns an iterator object that can be used to control the iteration.

Let's explore some practical examples to understand how generators are used:

Example 1: Fibonacci Sequence Generator

function* fibonacciGenerator() {
    let prev = 0;
    let curr = 1;

    yield prev;
    yield curr;

    while (true) {
        let next = prev + curr;
        yield next;
        prev = curr;
        curr = next;
    }
}

// Usage
const fibonacciSequence = fibonacciGenerator();
for (let i = 0; i < 10; i++) {
    console.log(fibonacciSequence.next().value); // Outputs: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
}
Enter fullscreen mode Exit fullscreen mode

In this example, fibonacciGenerator generates an infinite sequence of Fibonacci numbers, pausing after each yield statement until next() is called again.

Example 2: Inorder Traversal of a Binary Tree

class Node {
    constructor(value) {
        this.value = value;
        this.left = null;
        this.right = null;
    }
}

function* inorderTraversal(node) {
    if (node !== null) {
        yield* inorderTraversal(node.left);   // Traverse left subtree
        yield node.value;                     // Yield current node's value
        yield* inorderTraversal(node.right);  // Traverse right subtree
    }
}

// Usage
const rootNode = new Node(10);
rootNode.left = new Node(5);
rootNode.right = new Node(15);
// (Set up the rest of the tree as in the example)

const iterator = inorderTraversal(rootNode);
const result = [];
for (let value of iterator) {
    result.push(value);
}
console.log(result); // Outputs: [3, 5, 7, 10, 15, 18]
Enter fullscreen mode Exit fullscreen mode

This generator function performs an inorder traversal of a binary tree, yielding values in the correct order.

Example 3: Custom Iterator Using Generators

class Range {
    constructor(start, end) {
        this.start = start;
        this.end = end;
    }

    *[Symbol.iterator]() {
        for (let i = this.start; i <= this.end; i++) {
            yield i;
        }
    }
}

// Usage
const range = new Range(1, 5);
for (let num of range) {
    console.log(num); // Outputs: 1, 2, 3, 4, 5
}
Enter fullscreen mode Exit fullscreen mode

Here, the Range class uses a generator to create an iterable range of numbers from start to end.

Example 4: Asynchronous Control Flow

function fetchDataFromAPI(endpoint) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const data = { endpoint, results: [1, 2, 3, 4, 5] };
            resolve(data);
        }, Math.random() * 1000);
    });
}

function* fetchAndProcessData() {
    try {
        const data1 = yield fetchDataFromAPI('/api/data1');
        console.log('Processed data1:', data1);

        const data2 = yield fetchDataFromAPI('/api/data2');
        console.log('Processed data2:', data2);

        const data3 = yield fetchDataFromAPI('/api/data3');
        console.log('Processed data3:', data3);

        console.log('All data processed!');
    } catch (error) {
        console.error('Error:', error);
    }
}

function runGenerator(generator) {
    const iterator = generator();

    function iterate(iteration) {
        if (iteration.done) {
            return iteration.value;
        }

        const promise = iteration.value;
        return promise.then((value) => iterate(iterator.next(value)))
                      .catch((error) => iterator.throw(error));
    }

    return iterate(iterator.next());
}

// Usage
runGenerator(fetchAndProcessData);
Enter fullscreen mode Exit fullscreen mode

This example demonstrates using a generator to manage asynchronous operations sequentially, yielding promises and processing their results.

Conclusion

Generators offer a flexible way to control iteration and manage asynchronous flows in JavaScript. By pausing execution with yield, they allow complex tasks to be handled in a more readable and sequential manner. Understanding generators enhances your ability to write efficient and expressive JavaScript code.

Top comments (0)