The yield operator in JavaScript is used in conjunction with generators to control the flow of execution.
Generators allow you to pause and resume function execution, which is very useful in cases like asynchronous programming, lazy evaluation, or iterating over large data sets.
In this tutorial, you'll learn:
What is the yield operator?
How to create generator functions
How the yield operator works
Using yield* for delegating generators
Practical examples of the yield operator
1. What is the yield Operator?
The yield operator is used within a generator function to pause the function's execution and return a value to the calling code. When a generator is resumed, it continues from where it was last paused.
The generator function is defined using the function* syntax (a special syntax to indicate a generator).
Syntax:
yield expression
expression: The value to return from the generator. If omitted, yield returns undefined.
2. How to Create Generator Functions
A generator function is a special type of function that can be paused and resumed. It is defined using the function* keyword, and it returns a generator object when called.
Example 1: Basic Generator Function
function* myGenerator() { yield 1; yield 2; yield 3; } // Create a generator object const gen = myGenerator(); // Use next() to get values console.log(gen.next()); // Output: { value: 1, done: false } console.log(gen.next()); // Output: { value: 2, done: false } console.log(gen.next()); // Output: { value: 3, done: false } console.log(gen.next()); // Output: { value: undefined, done: true }
In this example:
yield pauses the function and returns a value.
next() is used to resume the generator and retrieve the next yielded value.
When all yield statements are executed, the done property becomes true, and the value is undefined.
3. How the yield Operator Works
The yield operator pauses the generator and allows the caller to control when to resume the execution. Each time next() is called, the generator resumes execution from where it was last paused.
Example 2: Yielding Multiple Values
function* countUpTo(limit) { for (let i = 1; i <= limit; i++) { yield i; } } const counter = countUpTo(3); console.log(counter.next().value); // Output: 1 console.log(counter.next().value); // Output: 2 console.log(counter.next().value); // Output: 3 console.log(counter.next().done); // Output: true
In this example, the generator yields values from 1 to 3. Each call to next() resumes execution until the limit is reached.
4. Using yield* for Delegating Generators
The yield* expression is used to delegate control to another generator or iterable object. This is useful when you want to include values from other generators within the current generator.
Example 3: Using yield* to Delegate to Another Generator
function* numbers() { yield 1; yield 2; yield 3; } function* moreNumbers() { yield* numbers(); // Delegate to the numbers generator yield 4; yield 5; } const gen = moreNumbers(); console.log(gen.next().value); // Output: 1 console.log(gen.next().value); // Output: 2 console.log(gen.next().value); // Output: 3 console.log(gen.next().value); // Output: 4 console.log(gen.next().value); // Output: 5 console.log(gen.next().done); // Output: true
In this example:
The moreNumbers() generator delegates to the numbers() generator using yield*.
The values from numbers() are yielded first, followed by additional values in moreNumbers().
5. Practical Examples of the yield Operator
Example 4: Infinite Sequence Generator
You can use the yield operator to create an infinite sequence that generates numbers indefinitely.
function* infiniteSequence() { let i = 0; while (true) { yield i++; } } const sequence = infiniteSequence(); console.log(sequence.next().value); // Output: 0 console.log(sequence.next().value); // Output: 1 console.log(sequence.next().value); // Output: 2 console.log(sequence.next().value); // Output: 3 // This can go on infinitely...
In this example, the generator produces an infinite sequence of numbers. Each call to next() returns the next number in the sequence.
Example 5: Generator for Lazy Evaluation
Generators can be used to process large datasets lazily, which means they only compute the next item when needed, reducing memory usage.
function* lazyRange(start, end) { for (let i = start; i <= end; i++) { yield i; } } const range = lazyRange(1, 1000); console.log(range.next().value); // Output: 1 console.log(range.next().value); // Output: 2 console.log(range.next().value); // Output: 3 // The generator processes only one value at a time, avoiding memory overflow for large ranges.
In this example, the generator lazyRange() yields numbers between start and end lazily, meaning that it generates each value only when it is requested.
Example 6: Bidirectional Communication with Generators
You can send values back into a generator using the next() method by passing an argument. The value passed becomes the result of the yield expression inside the generator.
function* interactiveGenerator() { const name = yield "What's your name?"; const age = yield `Hello, ${name}! How old are you?`; yield `You are ${age} years old.`; } const gen = interactiveGenerator(); console.log(gen.next().value); // Output: What's your name? console.log(gen.next("Alice").value); // Output: Hello, Alice! How old are you? console.log(gen.next(25).value); // Output: You are 25 years old.
In this example:
The generator interacts with the calling code by receiving values through next() and responding accordingly.
Each yield pauses the generator and waits for input.
Example 7: Iterating Over Object Properties with yield
You can use yield to iterate over object properties dynamically.
function* objectEntries(obj) { for (const key of Object.keys(obj)) { yield [key, obj[key]]; } } const person = { name: "Alice", age: 25, city: "Paris" }; const entries = objectEntries(person); console.log(entries.next().value); // Output: ['name', 'Alice'] console.log(entries.next().value); // Output: ['age', 25] console.log(entries.next().value); // Output: ['city', 'Paris'] console.log(entries.next().done); // Output: true
In this example:
The generator objectEntries() yields key-value pairs of an object.
Each call to next() returns the next key-value pair.
Example 8: Using Generators to Control Asynchronous Operations
Generators can be combined with promises to handle asynchronous tasks in a synchronous-like manner, although async/await is more common nowadays.
function* asyncGenerator() { const result1 = yield new Promise((resolve) => setTimeout(() => resolve('First Task Complete'), 1000)); console.log(result1); const result2 = yield new Promise((resolve) => setTimeout(() => resolve('Second Task Complete'), 1000)); console.log(result2); } function handleAsync(generator) { const gen = generator(); function process(yielded) { if (yielded.done) return; yielded.value.then((result) => { process(gen.next(result)); }); } process(gen.next()); } handleAsync(asyncGenerator); // Output after 1 second: First Task Complete // Output after 2 seconds: Second Task Complete
In this example:
The generator function asyncGenerator() performs asynchronous operations using promises.
The handleAsync() function handles promise resolution and resumes the generator when each promise is fulfilled.
Conclusion
The yield operator in JavaScript is a versatile tool that allows you to create generators, which can be paused and resumed, making them ideal for tasks like lazy evaluation, asynchronous control flow, and infinite sequences. Here's a summary of what you've learned:
The yield operator pauses execution within a generator and returns a value.
You can resume the generator using next().
The yield* operator allows you to delegate control to another generator or iterable.
Generators can be used for lazy evaluation, infinite sequences, asynchronous operations, and bidirectional communication.
By mastering the yield operator and generators, you can write more efficient and flexible code, especially when handling large data sets or asynchronous tasks!