The generator function has been a special function in JavaScript since 2015, but developers have overlooked it or rarely used it, perhaps due to its infrequent utilization or lack of familiarity. These special functions, capable of pausing and restarting execution, make handling async iteration and sequence easier. From lazy loading to custom iterables, animation to the beloved `await` keyword in asynchronous programming, generators are advanced concepts made simple.
This article introduces beginners to generator functions, explains their importance, and explores how they streamline complex tasks like lazy evaluation and creating custom iterables.
To fully grasp generator functions, let’s take a quick refresher on regular functions in JavaScript. This foundation will help us understand each step in this article and identify the key differences between the two.
What is a JavaScript Function?
In programming, a function is a reusable block of code that performs a specific task. You can call it multiple times with different inputs (if needed) to achieve the desired functionality, and it can optionally return a result.
Declaring a Function:
function functionName() {
}
Let’s define a simple function:
function calcRectArea(length, width) {
return length * width;
}
let result = calcRectArea(50, 80)
The function `calcRectArea` takes two parameters: length and width, representing the rectangle’s length and width, respectively. It calculates the rectangle’s area by multiplying the length and width and returns the result.
A standard function starts, runs, and returns when it finishes, while a generator function can pause and resume multiple times. Great! We just mentioned generator functions. Now, let’s learn what they are and how they differ from regular JavaScript functions.
What is a Generator Function in JavaScript?
Generator functions in JavaScript are powerful tools for generating sequences of values. They don’t return a single value like a regular function when called. Instead, they return a Generator object following the ES6 Iterable Protocol. This offers a simple way to handle large datasets or create custom iterators. It uses the yield keyword to pause execution and return values one at a time. This allows them to remember their state and resume execution from the exact pause point when called again.
In JavaScript, generator functions are defined using an asterisk (*) placed after the function keyword. The code syntax of a generator function declaration.
function* functionName(){
}
We can also declare a generator function in an expression like a regular function, as seen in the code below:
const generatorFunction = function*() {}
The generator function does not return a value immediately; instead, it returns a special object called a generator.
The generator object manages function execution through its `next()` method. The `yield` keyword within the generator function pauses execution and remembers its state. This allows the function to resume later, continuing from the pause point until completion.
Yield keyword :
function* functionName(){
yeild 'Name'
yeild 'Age'
yeild 'Employment status'
}
Next () method:
generator.next()
When we invoke the next() function, it yields an object comprising two properties:
* Value: This signifies the actual value of the object at the current iterator position.
* Done: This Boolean state indicates whether the iteration is complete or not.
{value: name, done: false|true}
Calling a Generator Function
To understand the concept further, let’s look at a basic code example with accurate data that shows how the generator function works. We will update the previous code with the accurate info we need.
function* profileInfo() {
yield 'Jane Smith';
yield 29;
yield 'Employed';
}
const generator = profileInfo()
generator.next()
generator.next()
generator.next()
The `profileInfo` function acts like a step-by-step information provider in our code example. It uses the `yield` keyword like a pause button, holding onto the information. When we call `profileInfo()`, it doesn’t reveal everything at once. Instead, it creates a special object that remembers its place and is ready to continue. We use the `next()` method to retrieve the information piece by piece. Each time we call it, the function resumes from where it left off and delivers the next yielded value.
If we log the generator object on our console, here’s what we will see:
{ value: 'Jane Smith', done: false }
{ value: 29, done: false }
{ value: 'Employed', done: false }
Calling the generator the fourth time will result in the value undefined and a true boolean value, indicating that there are no more elements to yield:
{ value: 'Jane Smith', done: false }
{ value: 29, done: false }
{ value: 'Employed', done: false }
{ value: undefined, done: true }
So, how does this all work together? Imagine you are interviewing someone and want to collect their information piece by piece. The first `next()` call is like asking for their name, and the generator yields ‘Jane Smith’. The second `next()` call would be like asking for their age, and it would yield 34. Finally, the third `next()` call might ask for their employment status and yield ‘Employed’.
Passing Arguments to Generator Functions for Dynamic Control
function* addNewEmpoyee(name, age, gender, department) {
yield `Creating new employee profile name: ${name}`;
yield `Setting age to: ${age}`;
yield `Setting gender to: ${gender}`;
yield `Selecting department to: ${department}`;
return `Employee profile: ${name}, age: ${age}, gender: ${gender}, department:${department} created.`;
}
const generator = addNewEmpoyee("John Doe", 30, "Male", "Technical"); // Pass arguments during creation
console.log(generator.next().value);
console.log(generator.next().value);
console.log(generator.next().value);
console.log(generator.next().value);
console.log(generator.next().value);
We define a generator function called `addNewEmployee`, which accepts four arguments: `name`, `age` , `gender`, `department` . Inside the function, we utilize four yield statements to yield each argument. Subsequently, we use the `return` keyword to return the four arguments. During instantiation, we pass the arguments, such as John Doe for the employee’s name, and so on, until we provide all four arguments. Finally, we invoke the generator function in our console using `generator.next()`. The output below is what we will get.
Creating new employee profile name: John Doe
Setting age to: 30
Setting gender to: Male
Selecting department to: Technical
Employee profile: John Doe, age: 30, gender: Male, department: Technical created.
In the previous code, we used the `return` keyword to return the final argument. Let’s provide a brief explanation of what the return keyword does.
Return keyword
The return statement is one of the methods used in a generator. When a generator function encounters a return statement, it exits the generator’s execution and returns a specific value. This effectively ends the generator’s ability to yield further values.
function* greetUser(name) {
yield "Hello!";
return name;
yield "How are you";
}
const generator = greetUser("John Doe");
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());
Code output
{ value: 'Hello!', done: false }
{ value: 'John Doe', done: true }
We noticed that we have two yield statements and a return statement. When we invoke the generator function three times, we receive a boolean value of true, indicating that the yield argument is complete. However, we won’t receive the last yield statement, which is “How are you?” Instead, we will get a response of `{ value: undefined, done: true }`
{ value: 'Hello!', done: false }
{ value: 'John Doe', done: true }
{ value: undefined, done: true }
This is because the returned value becomes the result of the next call on the generator object. Subsequent calls to .next() after the initial return will simply return a `{ value: undefined, done: true }` object, indicating the generator is finished, as seen in the code above. Therefore, when handling the return statement, exercise caution, as it terminates the function and returns the current value.
Lazy loading data with Generator Function
Generator functions in JavaScript provide an effective way to implement lazy loading by yielding values on demand. This pattern can improve performance, especially when dealing with large data sets or asynchronous data sources (API).
function* lazyLoadFunc(data) {
for (let item of data) {
yield item * 3;
}
}
// data array
const numbers = [1, 2, 3, 4, 5];
const lazyData = lazyLoadFunc(numbers);
// process the generator lazily
for (let printValues of lazyData) {
console.log(printValues);
}
Output
3
6
9
12
15
Creating a custom iterable object with a generator
The Generator function can be used to define an iterable object.
function* TrackeeEmployeeIterator(employees) {
for (let employee of employees) {
yield employee;
}
}
const TrackeeEmployeeDatabase = {
employees: [
{ id: 1, name: 'Gabriel', role: 'Backend Developer' },
{ id: 2, name: 'Emmanuel', role: 'Graphic Designer' },
{ id: 3, name: 'Jane', role: 'Community Manager' },
{ id: 4, name: 'Lora', role: 'Front-end Developer' },
],
[Symbol.iterator]: function() {
return TrackeeEmployeeIterator(this.employees);
}
};
for (let employee of TrackeeEmployeeDatabase ) {
console.log(employee);
}
We start by creating a function `TrackeeEmployeeIterator` that takes an argument for employees. This function is a generator, indicated by the function* syntax. We use a for loop inside this function to iterate over each employee in the employees array. The yield keyword returns each employee individually, allowing the generator to produce a sequence of employee objects.
Next, we define an array of employee objects, each with properties: `id, name, and role`. These objects represent individual employees.
To make the object iterable, we add a method `[Symbol.iterator]` to it. This special method returns an iterator created by the `TrackeeEmployeeIterator` generator function, passing in the employees array.
Finally, we use a `for…of` loop to iterate over the `TrackeeEmployeeDatabase` object. This loop utilizes the iterator to access and print each employee object to the console. The output is as shown below:
{ id: 1, name: 'Gabriel', role: 'Backend Developer' }
{ id: 2, name: 'Emmanuel', role: 'Graphic Designer' }
{ id: 3, name: 'Jane', role: 'Community Manager' }
{ id: 4, name: 'Lora', role: 'Front-end Developer' }
Other useful examples of generator functions are available in the MDN Web Docs.
Conclusion
Generator functions offer many advantages that help you write better functions. Let’s illustrate this: Think of a generator function like GraphQL. If you have a compiled list of data and need to display only a few items, GraphQL helps you filter the data you need instead of returning the entire list.
Similarly, the generator function helps you collect data piece by piece. If you have a long list of data and only need the first three items, you can pick them out. If you want to continue with the fourth item, you can resume, since the generator pauses the data and resumes from where you left off.