Table of Contents
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