ES6 Generators
ES6 Generators
Fu Cheng
Buy on Leanpub

Generator is a new concept introduced to JavaScript in ECMAScript 2015 (or ES6). Generators are the new powerful tools which should be in each JavaScript developer’s toolkit. Generators are mostly used in JavaScript frameworks and libraries. For example, Koa uses generators as middleware. Babel can use generators to transpile async/await functions. But generators are not commonly used in application code yet. The main reason is that generators are not easy to understand and adopt in day-to-day development.

This book focuses on real day-to-day development scenarios which can benefit from using generators.

Most the code examples in this book are tested on Chrome browser and some of them are tested on NodeJS 6.7.0. These examples should also work on other browsers which support generators. Refer to this page for browser compatibility of generators.

I Generators basics

Before discussing actual usage of generators, we start from the basic concept of generators.

There are two different concepts related to generators.

  • Generator function - A special kind of function which generates generator objects.
  • Generator object - An instance of generator function.

Execution of generator objects can be suspended and resumed. In JavaScript, we have only limited control over execution of normal functions. Given a function, when it starts execution, by using (), apply or call, it will run to the end of the execution.

For a simple function sum shown below, when it’s invoked using sum(1, 2), it starts execution and returns value 3 to the caller.

1 function sum(a, b) {
2     return a + b;
3 }
4 
5 let result = sum(1, 2);
6 // -> 3

As JavaScript engine execution is single-threaded (not considering web worker here), during the execution of a function, there is no way to stop the execution. So if you accidentally create an infinite loop in your function, the whole application will be blocked.

1. Basic generators

Let’s start with a simple generator function. The difference between a generator function and a normal function declaration is the * between function and the function name.

1 function *sample() {
2   yield 1;
3   yield 2;
4   yield 3;
5 }

Generator objects can return multiple values when next() method is invoked. Those values are specified using yield keyword. In the generator function above, three yield expressions can generate three values 1, 2 and 3 when next() method of a generator object is invoked.

 1 let func = sample();
 2 func.next();
 3 // -> {value: 1, done: false}
 4 func.next();
 5 // -> {value: 2, done: false}
 6 func.next();
 7 // -> {value: 3, done: false}
 8 func.next();
 9 // -> {value: undefined, done: true}
10 func.next();
11 // -> {value: undefined, done: true}

In the code above, invoking the generator function sample generates a new generator object func. Execution of generator object func is initially suspended. When next method is invoked on the func object, it starts execution and runs to the first yield expression and returns the value 1 to the caller. The return value is an object with two properties: value and done. value contains the return value of yield expression, done can be used to check if there are more values to get. done property is false for the first three invocations of next method. For the fourth invocation, done property is set to true, which means there are no values anymore.

1.1 Suspend & resume execution

The power of generators comes from the ability to suspend and resume execution of generator objects. Each generator object can be viewed as a state machine. Each instance of the same generator function maintains its own state. Invoking next() on the generator object triggers state transition inside the object, which causes the object runs to the next yield expression. This continues until no more yield expressions found.

In the code below, two generator objects func1 and func2 maintain their own internal states. Invoking next() on one object doesn’t affect the state of the other object.

1 let func1 = sample();
2 let func2 = sample();
3 func1.next();
4 // -> {value: 1, done: false}
5 func2.next();
6 // -> {value: 1, done: false}
7 func1.next();
8 // -> {value: 2, done: false}

1.2 Check types of generator functions and generator objects

We can use Object.prototype.toString to check the types of generator functions and generator objects.

1 function *sample() {
2 
3 }
4 
5 Object.prototype.toString.apply(sample);
6 // -> "[object GeneratorFunction]"
7 
8 Object.prototype.toString.apply(sample());
9 // -> "[object Generator]"

2. Pass values to next()

Let’s start from another simple generator function doMath. If we just look at the code, we may think that after invoking next() on the generator object, the value of x should be 1, the value of y should be 11 and the value of z should be 110. It’s just simple math, right???

1 function *doMath() {
2   let x = yield 1;
3   let y = yield x + 10;
4   let z = yield y * 10;
5 }

But the actual result doesn’t match what we would expect. As shown in the code below, the values are 1, NaN and NaN.

1 let func = doMath();
2 func.next();
3 // -> {value: 1, done: false}
4 func.next();
5 // -> {value: NaN, done: false}
6 func.next();
7 // -> {value: NaN, done: false}
8 func.next();
9 // -> {value: undefined, done: true}

The key to understanding the actual result is that value passed to next() invocation is the actually used value of last yield expression. Since we didn’t pass any argument when invoking next(), so the value of each yield expression is actually undefined.

For the first next() invocation, there is no last yield expression, so the value is actually ignored. For the second next() invocation, value of last yield expression, i.e. yield 1 is set to undefined, which sets x to undefined, then sets the result of yield x + 10 to NaN. For the third next() invocation, value of last yield expression, i.e. yield x + 10 is set to undefined, which sets y to undefined, then sets the result of yield y * 10 to NaN.

Now we can try to pass a value when invoking next() method on a generator object. In the code below, the second next() invocation func.next(1) passes 1 to the generator object, so value 1 is set as the value of yield 1, which sets x to 1, then the result of this next() will be 11. For the third next() invocation func.next(2), 2 is passed as the value of yield x + 10, which sets y to 2, then the result of this next() will be 20.

1 let func = doMath();
2 func.next();
3 // -> {value: 1, done: false}
4 func.next(1);
5 // -> {value: 11, done: false}
6 func.next(2);
7 // -> {value: 20, done: false}
8 func.next(3);
9 // -> {value: undefined, done: true}

3. return in generators

In the generator function, we can also use return statement. The returned value is also passed to the caller of a generator object’s next() method. return also finishes execution of generator object, i.e. done property is set to true. In the code below, the return value of second next(1) invocation is the value of return statement, i.e. x + 2.

 1 function *withReturn() {
 2   let x = yield 1;
 3   return x + 2;
 4 }
 5 
 6 let func = withReturn();
 7 func.next();
 8 // -> {value: 1, done: false}
 9 func.next(1);
10 // -> {value: 3, done: true}
11 func.next();
12 // -> {value: undefined, done: true}

3.1 Infinite values

It’s possible for a generator object to generate an infinite number of values, i.e. done property is always false. For example, we can create a generator which generates infinite integer numbers starting from 0. In this case, we can use return to finish generator objects.

In the code below, loop keeps generating incremental values in a while loop. When a truthy value is passed to next() as the value of shouldExit, the last value is returned and generator object is finished.

1 function *loop() {
2   var count = 0;
3   while (true) {
4     let shouldExit = yield count++;
5     if (shouldExit) {
6       return count++;
7     }
8   }
9 }

As shown in the code below, three values are generated using next(). The forth next(true) invocation finishes the generator object func.

 1 let func = loop();
 2 func.next();
 3 // -> {value: 0, done: false}
 4 func.next();
 5 // -> {value: 1, done: false}
 6 func.next();
 7 // -> {value: 2, done: false}
 8 func.next(true);
 9 // -> {value: 3, done: true}
10 func.next();
11 // -> {value: undefined, done: true}

4. Iterators & generators

From all the generators code above, you may wonder why we should use next() to get values from the generator objects and deal with the nonintuitive return value format {value: 1, done: false}. Meet iterators.

4.1 Iterators

Iterators are no strangers to developers. They already exist in different programming languages with similar names, e.g. Java Iterator, Ruby Enumerator and Python Iterator Types. Iterators can be used to iterate over items in a collection. Iterators maintain their own states regarding the current position in the target collection.

An iterator in ES6 is just an object which provides a next() method to get next item in the current iteration. next() method should return an object with two properties: value and done. So generator functions are actually factories of iterators.

4.2 Iterables

Iterables are objects which have property @@iterator. The value of @@iterator property is a function that returns an Iterator object.

A generator object conforms to both the Iterator and Iterable interfaces.

4.3 Iterate generator objects

As generators are iterable, we can use other ES6 language features to interact with generator objects easily. Following examples use values generator function shown below.

1 function *values() {
2   yield 'a';
3   yield 'b';
4   yield 'c';
5 }

for-of loops

We can use for-of loops to easily iterate all the values in a generator object.

1 for (let value of values()) {
2   console.log(value);
3 }
4 // -> Output 'a', 'b' and 'c'

Spread operator

Generator objects can also be used with spread operator.

 1 // Spread operation in array literals
 2 [1, ...values(), 2]
 3 // -> [1, "a", "b", "c", 2]
 4 
 5 // Spread operation in function calls
 6 function join(x, y, z) {
 7   return x + y + z;
 8 }
 9 join(...values());
10 // -> "abc"

Work with new collection types

Generator objects can be used to create new collection objects, e.g. Set, WeakSet, Map and WeakMap.

1 let set = new Set(values());
2 set.size;
3 // -> 3
4 
5 set.forEach(function(value) {
6   console.log(value);
7 });
8 // -> Output 1, 2, 3