DEV Community

loading...
Cover image for JavaScript Generator Functions Made Simple

JavaScript Generator Functions Made Simple

alexdevero profile image Alex Devero Originally published at blog.alexdevero.com ・10 min read

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 has been around since the beginning of JavaScript. Functions are one of the fundamental building blocks of this programming language. This is not true if we talk about generator functions. These special functions have been introduced to JavaScript quite recently.

Functions have been working very well. Aside to regular functions, there are now also arrow functions. So far, arrow functions proved to be beneficial in some cases. They are often also preferred by JavaScript developers over the regular. One may ask, why add something new?

Both regular and arrow functions are great when you want to encapsulate a piece of code to make it re-usable. They are also allow you to return a single value or nothing. A single can be also an array or an object that contains multiple values. Nonetheless, there is still one thing you want to return.

This is where generator functions are different. Unlike regular functions, generators are capable of returning multiple values. That said, they don't return all of them at the same time. Instead, they returned one after another, only when you want it. Until then, generator will wait while remembering the last value.

The syntax

One nice thing about generators is that they have a friendly syntax. There is a lot to learn, especially if you already know something about regular functions. At this monument, there are two ways to create a generator function. The first one is with the help of GeneratorFunction constructor.

This approach is not very common and you will see it very rarely. The second, and more common approach, is by using function declaration. Yes, you can also create generators with function expression. In both cases, the function keyword is followed by asterisk (*).

This symbol is what tells JavaScript that you want to create a generator function, not a regular function. Except this small change, the rest of the syntax is the same as for function. There is the function name, parentheses for parameters, and function body with code to execute.

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

The generator object

One thing that may surprise you is that generators don't run the code inside them when you call them. Calling a generator function doesn't execute the code inside it. Instead, the generator function will return a special object called "generator object". This object allows you to work with the generator.

It allows you to tell the generator to return new value when you need it, by referencing this object. Because of this, when you call a generator function you should assign the returned generator object to some variable. Otherwise, it will be lost.

// 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] {}
Enter fullscreen mode Exit fullscreen mode

The yield and next()

When it comes to generator functions, there are two important things. The first is yield keyword, along with some expression. The second is next() method. The yield keyword is like a breakpoint. You can use it only in a generator function. It does two things. First, it returns a value from the generator.

The second thing it does is it pauses execution of the generator. This happens right after the generator returns the value. You can think about the yield as a return statement. The difference is that while the return returns and terminates a function, yield returns and only pauses a generator.

As you know, calling a generator function returns a generator object. The next() is the main method of this generator object. This method allows you to run the generator, execute the code inside, and return some value. The value it returns is specified by the yield keyword. It is preceded by it.

So, to sum it up. The yield allows you to return a value from the generator, when you execute it, and then pause the generator. The next() method allows you to execute the generator, return the value that follows after the yield. Remember that next() will return only the value after the first yield.

If you use five yield keywords in a generator, you will have to call the next() method five times. One call for one yield. With every yield the execution of a generator will be paused, waiting for another call of next() method.

// 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 }
Enter fullscreen mode Exit fullscreen mode

Yield, next, value and done

When you call the next() method JavaScript will always return an object. This object will contain two properties with some values. One property will be value. This is the actual value returned by the generator. It is the value that follows after the yield keyword in your code.

If there is no value to return, the value of this property will be undefined. The second property is done. This property says if the generator function is finished or not. "Finished" means that there are no more yield keywords in the generator functions and no more values to return.

The value of done will always be a boolean, either true or false. It will be false until the generator reaches the last yield. When it does, it will return the last value, after the last yield, along with done set to true. After this, calling next() again will be useless.

// 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.
Enter fullscreen mode Exit fullscreen mode

Yield and return

Just because generators use yield to return values doesn't mean there is no place for return statement. There still is. In the context of generator functions, a return statement, is another thing that specifies if the generator is finished. Generator will finish under two conditions.

First, there are no more yield keywords. Second, the execution encounters a return statement. These two will change the value of done in the returned object from false to true. Returning a value with return works like yield. Any value after the return will be the value of value property in the returned object.

Three things to remember. First, return will finish the generator whether there are other yield or not. Let's say you declare four yields in a generator, but put return after the second. The result will that the generator will return three values. Two values for the first two yield and one for the return.

The last two yield after the return statement will never be executed because the return will finish the generator prematurely. 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.

The third to remember. If you don't use return, the value of done after the last yield will be still set to false. It will change only if you try to return a value one more time. With return, done will be set to true right with the last call of 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 }
Enter fullscreen mode Exit fullscreen mode

Example of generator function with a loop

This ability to return a value on demand can be useful for example when you want to generate a series of numbers with loop. Normally, the loop would return all numbers right away. This will not happen if you use generator functions. Generators will allow you to return all numbers one by one.

There are only few things you need to create this number generator. First, is a generator function. Inside this generator will be a loop. Inside this loop will be the yield keyword returning current number in the series. This will create a loop that will pause after every iteration, waiting for the next call of next().


// 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.
Enter fullscreen mode Exit fullscreen mode

Yield* and execution delegation

The yield keyword can do more than just return a value. It also allows you to delegate the execution of generator to another generator. You can use it to start a second generator from the first. This second generator will run until it is finished. Then, the execution will resume to the first generator.

This allows you to connect multiple generators together. Then, you can run them in a series you can control, each as long as necessary. When you want to use the yield to do this, just remember to add the asterisk symbol (*) right after the yield keyword (yield*). Then add a call to the generator you want to execute.

// 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 }
Enter fullscreen mode Exit fullscreen mode

Yield* and return statement

When you use delegation careful with return statements. This applies especially to generators somewhere in the series. Don't worry. The return statement will not finish, or terminate, the whole chain. It will only finish the generator at which it is. However, it will not return any value.

When you use return statement in a generator it will finish the generator. It will also return a value that follows it. This doesn't applies to delegated execution and chain of generators. In this case, return will only finish the current generator and resume execution to the previous one. It will not return a value.

// 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 }
Enter fullscreen mode Exit fullscreen mode

Yield, next() and passing arguments

There is one interesting thing about the next() method. It allows you to pass arguments to generator functions. When you pass something as an argument to the next(), that value will become the value of yield in the generator. That said, if you want to pass some argument, do it for the second call of next(), not the first.

The reason for this is simple. The first call of next() method starts the execution of the generator. The generator is then paused when it reaches the first yield. There is no yield between the start of the generator execution and the first yield. So, any argument you pass will be lost.

// 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 }
Enter fullscreen mode Exit fullscreen mode

Conclusion: JavaScript Generator Functions Made Simple

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.

Discussion (2)

pic
Editor guide
Collapse
ramprakashram profile image
Ram

This is one of the best and cleanest explanation of generator functions I have seen.

Collapse
jonrandy profile image
Jon Randy

Surprised you didn't mention they can be used with for...of