JavaScript generator functions are one of the less-known features of JavaScript. However, in certain situations, they can provide an elegant way of implementing functionality in a clean and concise manner. In the sections below, well introduce the concepts and review how to create and use generator functions with code examples.
Generator Functions
A generator function is a function which can yield values before finally returning a value. In this article, we will refer to a value which is yielded or returned as an output. Normal JavaScript functions can only output one value or object, but generator functions can output any number of values or objects.
Use Cases
Generator functions are used in a few types of situations, including but not limited to:
- Lazy evaluation: only execute code when the consumer requests the generator to yield the next value.
- Infinite iterables: dynamically and lazily generate iterable values as we need them, such as a list of all natural numbers (until we stop generating them).
- Identifier generation: one way to create unique identifiers is to share a generator and ask it to yield a new value when a new identifier is needed.
- Async and await: JavaScript's async functions are driven by generator functions behind the scenes.
Coding with Generator Functions
A generator function is created in the same manner as a normal function with two exceptions:
- Instead of declaring as a function, we will declare it as a function*. Notice the asterisk at the end of the function keyword.
- When the function should output without returning, it will use the yield keyword instead of return.
We can only create generator functions with the function* declaration; arrow / lambda syntax cannot be used in this case. The example below shows the creation and invocation of a generator function which outputs integers:
Notice that after we've called our generator function, we store its return value into the variable intG, then call next on that variable instead of the function itself. This is a relationship analagous to, but not equal to, the class vs instance relationship. Also notice that the execution of the generator function is paused after each yield keyword. Once .next is called, execution is resumed again until the next yield keyword is reached.
Generator Functions with Parameters
Generator functions accept parameters in the same manner as normal functions. In the example below, our generator function will take two arguments. Notice that the arguments are being passed in when we invoke randomNumberGenerator and not when we invoke next.
Notice the objects which were console logged were not the exact same as what we've yielded. In the next section, we'll discuss why.
Data Type of Yielded Values
When a generator yields a value, the return type of the .next invocation will be an object which has two properties:
- value: the actual value which was given to the yield statement inside of the generator function.
- done: a boolean which will be false if a value is yielded or true if a value is returned.
When the field done is true, the generator function has concluded and should not be invoked again. If it is, no code in the generator itself will be invoked, and the .next call will return a value of undefined. The example below exemplifies this behavior:
The done property enables the consumer to know when the generator has concluded and act appropriately. In some situations, such as our first example, the generator will never conclude. Whether any given generator will conclude or not depends on what it is designed to generate and if it makes sense for it to eventually conclude or continue generating for any arbitrary amount of time.
Passing Arguments into Next Calls
Generator functions allow the consumer to pass in arguments when calling .next. The function receives the input via the yield keyword. The value on the right side of yield is emitted when .next is called, and the value passed into the following .next call will be assigned to the variable on the left side of yield. The generator function below showcases this behavior:
Notice that the first argument appears to be missing! This is because the first .next invocation is where the function starts executing, but we do not receive any values from .next via yield until the second next call is made. To properly utilize inputs via yields, keep in mind that the second next call will result in the first value returned by yield within the generator function itself.
Nested Generators
We can nest generators automatically using a yield* keyword (notice the "*"). If we call yield* on a generator itself, JavaScript will have the containing generator forward calls on its own .next to that generator until it is done, then will resume with its own execution. Note: the value which is returned, not yielded, by the nested generator will be ignored by the containing generator. Notice in the example below, the smallGenerator returns "goodbye", but that string never appears in the console log: