What are generator functions in JavaScript...

Photo by Growtika on Unsplash

What are generator functions in JavaScript...

Generator functions are one of those things you may not see as often. However, they can be handy in some situations. This tutorial will help you understand them. You will learn about what generator functions are. You will also learn about yield and next() and also how to delegate execution.

From regular functions to generator functions:

Regular functions have been a part of JavaScript since the beginning and are essential in the language. However, generator functions are a newer addition to JavaScript.

Regular functions work well and are used to encapsulate code for reusability. They can return a single value or nothing at all. This value can be an array or an object with multiple values, but ultimately, only one thing is returned.

On the other hand, generator functions are different. They can return multiple values, but not all at once. Instead, they return one value at a time, and they wait until you ask for the next value before giving it to you. The generator function remembers the last value it returned until you're ready for the next one.

The syntax:

Generators have a user-friendly syntax that makes them easy to work with. If you already know about regular functions, there are a few things to learn, but it's not too complicated.

To create a generator function, there are two ways you can do it. The first way, which is not very common, is by using the GeneratorFunction constructor. You won't see this approach used often.

The second and more common way is by using function declaration. You can also create generators using function expression. In both cases, you start with the "function" keyword, followed by an asterisk (*).

The asterisk is what tells JavaScript that you want to create a generator function, not a regular function. Other than that small change, the rest of the syntax is the same as creating a regular function. You have the function name, parentheses for parameters, and the function body where you write the code you want to execute.

// Create generator function:
function* myGenerator() {
  // Function body with code to execute.
}

The generator object:

One surprising thing about generators is that when you call them, they don't immediately run the code inside. Instead, calling a generator function gives you a special object called a "generator object." This object allows you to interact with the generator.

With the generator object, you can ask the generator to give you a new value when you want it. It's important to remember that when you call a generator function, you should assign the returned generator object to a variable. If you don't, you will lose access to it.

// Create generator function:
function* myGenerator() {
  // Function body with code to execute.
}

// Assign the generator object to variable:
const myGeneratorObj = myGenerator()

// Log the generator object:
console.log(myGeneratorObj)
// Output:
// Iterator [Generator] {}

The yield and next():

When working with generator functions, there are two important things to understand. The first is the yield keyword, along with an expression. The second is the next() method.

The yield keyword is like a pause button that you can only use inside a generator function. It does two things. First, it returns a value from the generator. Second, it temporarily stops the execution of the generator right after returning the value. Think of yield as a special kind of return statement. The difference is that return ends a function, while yield only pauses a generator.

When you call a generator function, it gives you a generator object. The next() method is the main method of this object. It allows you to run the generator, execute its code, and get a value in return. The value is specified by the yield keyword that comes before it.

To summarize, yield lets you return a value from the generator and pause its execution. The next() method lets you execute the generator, retrieve the value that comes after the yield, and pause again. It's important to remember that next() will only return the value after the first yield it encounters.

If you have five yield keywords in a generator, you will need to call the next() method five times. Each call corresponds to one yield. Each time you call "next(), the generator execution pauses and waits for the next call.

// Create generator function:
function* myGenerator() {
  // Use yield to return values:
  yield 1
  yield 2
  yield 3
  yield 4
  return 5
}

// Assign the generator object to variable:
const myGeneratorObj = myGenerator()

// Return the first value:
console.log(myGeneratorObj.next())
// Output:
// { value: 1, done: false }

// Return the second value:
console.log(myGeneratorObj.next())
// Output:
// { value: 2, done: false }

// Return the third value:
console.log(myGeneratorObj.next())
// Output:
// { value: 3, done: false }

// Return the fourth value:
console.log(myGeneratorObj.next())
// Output:
// { value: 4, done: false }

// Return the fifth value:
console.log(myGeneratorObj.next())
// Output:
// { value: 5, done: true }
// The generator is finished.

// Try to return one more time:
console.log(myGeneratorObj.next())
// Output:
// { value: undefined, done: true }

Yield, next, value and done:

When you call the next() method in JavaScript, it always returns an object with two properties and their corresponding values.

The first property is called value. It represents the actual value returned by the generator. This value is the one that comes after the yield keyword in your code. If there's no value to return, the value property will be undefined.

The second property is called done. It tells you whether the generator function has finished or not. If there are no more yield keywords in the generator function and no more values to return, then it is considered finished. The done property will always be a boolean value, either true or false. It will be false until the generator reaches the last yield. Once it reaches the last yield, it will return the final value after that yield and set done to true. After this point, calling next() again will have no effect.

// Create generator function:
function* myGenerator() {
  // Use yield to return values:
  yield 'a'
  yield 'b'
  return 'omega'
}

// Assign the generator object to variable:
const myGeneratorObj = myGenerator()

// Return the first value:
console.log(myGeneratorObj.next())
// Output:
// { value: 'a', done: false }

// Return the second value:
console.log(myGeneratorObj.next())
// Output:
// { value: 'b', done: false }

// Return the third value:
console.log(myGeneratorObj.next())
// Output:
// { value: 'omega', done: true }
// This is the last value returned
// and the generator is finished.

Yield and return:

Even though generators use yield to return values, the return statement still has a role to play. It determines if the generator is finished. A generator can finish in two ways.

First, when there are no more yield keywords. Second, when the execution encounters a return statement. These two conditions will change the value of the done property in the returned object from false to true. When you use return to specify a value, it works similarly to yield. The value after the return statement becomes the value of the value property in the returned object.

Here are three important things to remember. First, return will end the generator regardless of whether there are other yield statements or not. For example, if you have four yield statements in a generator but put return after the second one, the generator will return three values. Two values for the first two yield statements and one for the return statement. The last two yield statements after the return will never be executed because the return ends the generator early.

The second thing to remember is that you don't necessarily have to use the return statement. The generator will finish when it encounters the last yield statement.

The third thing to remember is that if you don't use return, the value of done after the last yield will still be set to false. It will change to true only if you try to return a value one more time. With return, done will be set to true when you make the last call to the next() method.

// Generator function without return:
// NOTE: last yield will not change "done" to "true".
// It will change only after another call of "next()".
function* myGeneratorOne() {
  // Use yield to return values:
  yield 'a'
  yield 'b'
}

// Assign the generator object to variable:
const myGeneratorOneObj = myGeneratorOne()

// Return the first value:
console.log(myGeneratorOneObj.next())
// Output:
// { value: 'a', done: false }

// Return the second value:
console.log(myGeneratorOneObj.next())
// Output:
// { value: 'b', done: false }

// Try to return value again:
console.log(myGeneratorOneObj.next())
// { value: undefined, done: true }
// The generator is finished.


// Generator function ending with return:
// NOTE: the return will change "done" to "true" right away.
function* myGeneratorOne() {
  // Use yield to return values:
  yield 'a'
  return 'b'
}

// Assign the generator object to variable:
const myGeneratorOneObj = myGeneratorOne()

// Return the first value:
console.log(myGeneratorOneObj.next())
// Output:
// { value: 'a', done: false }

// Return the second value:
console.log(myGeneratorOneObj.next())
// Output:
// { value: 'b', done: true }
// The generator is finished.


// Generator function with return in the middle:
function* myGeneratorOne() {
  // Use yield to return values:
  yield 'a'
  yield 'b'
  return 'End'
  yield 'c'
  yield 'd'
}

// Assign the generator object to variable:
const myGeneratorOneObj = myGeneratorOne()

// Return the first value:
console.log(myGeneratorOneObj.next())
// Output:
// { value: 'a', done: false }

// Return the second value:
console.log(myGeneratorOneObj.next())
// Output:
// { value: 'b', done: false }

// Return the third value (the return):
console.log(myGeneratorOneObj.next())
// Output:
// { value: 'End', done: true }
// The generator is finished.

// Try to return the fourth value:
console.log(myGeneratorOneObj.next())
// Output:
// { value: undefined, done: true }

Example of generator function with a loop:

The ability to return values on demand can be handy, especially when you want to generate a series of numbers using a loop. Normally, a loop would return all the numbers at once. However, with generator functions, you can return each number one by one.

To create a number generator using a generator function, you only need a few things. First, you need the generator function itself. Inside this function, you'll have a loop. Within that loop, you'll use the yield keyword to return the current number in the series. This setup creates a loop that pauses after each iteration, waiting for the next next() call to continue and generate the next number in the series.

<br />// Example of generator with for loop:
function* myGenerator() {
  for (let i = 0; i < 5; i++) {
    yield i
  }
}

// Assign the generator object to variable:
const myGeneratorObj = myGenerator()

// Return the first number:
console.log(myGeneratorObj.next())
// Output:
// { value: 0, done: false }

// Return the second number:
console.log(myGeneratorObj.next())
// Output:
// { value: 1, done: false }

// Return the third number:
console.log(myGeneratorObj.next())
// Output:
// { value: 2, done: false }

// Return the fourth number:
console.log(myGeneratorObj.next())
// Output:
// { value: 3, done: false }

// Return the fifth number:
console.log(myGeneratorObj.next())
// Output:
// { value: 4, done: false }

// Try to return another number:
console.log(myGeneratorObj.next())
// Output:
// { value: undefined, done: true }
// The generator is finished.

Yield* and execution delegation:

The ability to return values on demand can be helpful when you want to generate a series of numbers using a loop. Normally, a loop would return all the numbers at once. However, with generator functions, you can return the numbers one by one.

To create this number generator, you need a few things. First, you need a generator function. Inside this function, you can have a loop. Within the loop, you use the yield keyword to return the current number in the series. This creates a loop that pauses after each iteration, waiting for the next call to next() in order to continue generating the next number.

// Create first generator function:
function* myGeneratorOne() {
  yield 1
  yield* myGeneratorTwo() // Delegate to myGeneratorTwo() generator.
  yield 3
}

// Create second generator function:
function* myGeneratorTwo() {
  yield 'a'
  yield 'b'
  yield 'c'
}

// Assign the first generator object to variable:
const myGeneratorObj = myGeneratorOne()

// Return the first value (myGeneratorOne):
console.log(myGeneratorObj.next())
// Output:
// { value: 1, done: false }

// Return the second value (myGeneratorTwo):
console.log(myGeneratorObj.next())
// Output:
// { value: 'a', done: false }

// Return the third value (myGeneratorTwo):
console.log(myGeneratorObj.next())
// Output:
// { value: 'b', done: false }

// Return the fourth value (myGeneratorTwo):
console.log(myGeneratorObj.next())
// Output:
// { value: 'c', done: false }

// Return the fifth value (myGeneratorOne):
console.log(myGeneratorObj.next())
// Output:
// { value: 3, done: false }

// Return the sixth value (myGeneratorOne):
console.log(myGeneratorObj.next())
// Output:
// { value: undefined, done: true }

Yield* and return statement:

When using delegation, you need to be cautious with return statements, especially when there are generators involved in the series. But don't worry, the return statement won't end or terminate the entire chain of generators. It will only end the generator where it is used, without returning any value.

If you use a return statement in a generator, it will finish that specific generator and stop its execution. It will also return a value immediately after the return statement. However, when it comes to delegated execution and a chain of generators, the return statement behaves differently. In this case, the return statement only ends the current generator and resumes execution in the previous one. It does not return a value as it normally would.

// Create first generator function:
function* myGeneratorOne() {
  yield 1
  yield* myGeneratorTwo() // Delegate to myGeneratorTwo() generator.
  yield 3
}

// Create second generator function:
function* myGeneratorTwo() {
  yield 'a'
  yield 'b'
  return 'c' // This returned value will not show up.
}

// Assign the first generator object to variable:
const myGeneratorObj = myGeneratorOne()

// Return the first value (myGeneratorOne):
console.log(myGeneratorObj.next())
// Output:
// { value: 1, done: false }

// Return the second value (myGeneratorTwo):
console.log(myGeneratorObj.next())
// Output:
// { value: 'a', done: false }

// Return the third value (myGeneratorTwo):
console.log(myGeneratorObj.next())
// Output:
// { value: 'b', done: false }

// Return the fourth value (myGeneratorOne):
console.log(myGeneratorObj.next())
// Output:
// { value: 3, done: false }

// Return the fifth value (myGeneratorOne):
console.log(myGeneratorObj.next())
// Output:
// { value: undefined, done: true }

Yield, next() and passing arguments:

The next() method has an interesting feature—it allows you to pass arguments to generator functions. When you pass an argument to next(), that value will be assigned as the value of the corresponding yield in the generator. However, it's important to note that if you want to pass an argument, you should do it for the second call of next(), not the first.

The reason for this is simple. The first call to next() starts the execution of the generator. The generator then pauses when it encounters the first yield. Between the start of the generator execution and the first yield, there are no other yield statements. Therefore, any argument you pass during the first call will be lost and not associated with any yield statement.

// Create generator function:
function* myGenerator() {
  console.log(yield + 1)
  console.log(yield + 2)
  console.log(yield + 3)
  console.log(yield + 4)
  return 5
}

// Assign the first generator object to variable:
const myGeneratorObj = myGenerator()

// Return the first value (no argument passing):
console.log(myGeneratorObj.next())
// Output:
// { value: 1, done: false }
// '1x' // <= value from console.log(yield + ...)

// Return the second value:
console.log(myGeneratorObj.next('1x'))
// Output:
// { value: 2, done: false }
// '2x' // <= value from console.log(yield + ...)

// Return the third value:
console.log(myGeneratorObj.next('2x'))
// Output:
// { value: 3, done: false }
// '3x' // <= value from console.log(yield + ...)

// Return the fourth value:
console.log(myGeneratorObj.next('3x'))
// Output:
// { value: 4, done: false }
// '4x' // <= value from console.log(yield + ...)

// Return the fifth value:
console.log(myGeneratorObj.next('4x'))
// Output:
// { value: 5, done: true }

Conclusion:

Generator functions may not be used as often, but they can be useful. For example, when you want to generate some data on demand. Or, when you want to have more control over iteration over some data. I hope that this tutorial helped you understand what generator functions are and how to work with them.

See you in the next one....