97
is the value of the named property. Because this object is an iterable iterator, you can
use it with a
for/in
loop instead of calling its
next()
method directly, and this means
that you can use the
Iterator()
function along with destructuring assignment to con-
veniently loop through the properties and values of an object or array:
for(let [k,v] in Iterator({a:1,b:2})) // Iterate keys and values
console.log(k + "=" + v); // Prints "a=1" and "b=2"
There are two other important features of the iterator returned by the
Iterator()
func-
tion. First, it ignores inherited properties and only iterates “own” properties, which is
usually what you want. Second, if you pass
true
as the second argument to
Iterator()
, the returned iterator will iterate only property names, not property values.
The following code demonstrates these two features:
o = {x:1, y:2} // An object with two properties
Object.prototype.z = 3; // Now all objects inherit z
for(p in o) console.log(p); // Prints "x", "y", and "z"
for(p in Iterator(o, true)) console.log(p); // Prints only "x" and "y"
11.4.3 Generators
Generators are a JavaScript 1.7 feature (borrowed from Python) that use a new
yield
keyword, which means that code that uses them must explicitly opt in to version 1.7,
as described in §11.2. The
yield
keyword is used in a function and functions something
like
return
to return a value from the function. The difference between
yield
and
return
, however, is that a function that yields a value to its caller retains its internal
state so that it is resumable. This resumability makes
yield
a perfect tool for writing
iterators. Generators are a very powerful language feature, but they can be tricky to
understand at first. We’ll begin with some definitions.
Any function that uses the
yield
keyword (even if the
yield
is unreachable) is a gener-
ator function. Generator functions return values with
yield
. They may use the
return
statement with no value to terminate before reaching the end of the function body, but
they may not use
return
with a value. Except for their use of
yield
, and this restriction
on the use of
return
, generator functions are pretty much indistinguishable from regular
functions: they are declared with the
function
keyword, the
typeof
operator returns
“function”, and they inherit from
Function.prototype
just as ordinary functions do.
When invoked, however, a generator function behaves completely differently than a
regular function: instead of executing the body of the generator function, the invocation
instead returns a generator object.
A generator is an object that represents the current execution state of a generator func-
tion. It defines a
next()
method that resumes execution of the generator function and
allows it to continue running until its next
yield
statement is encountered. When that
happens, the value of the
yield
statement in the generator function becomes the return
value of the
next()
method of the generator. If a generator function returns (by exe-
cuting a
return
statement or reaching the end of its body), the
next()
method of the
generator throws
StopIteration
.
11.4 Iteration | 277
Core JavaScript
82
The fact that generators have a
next()
method that can throw
StopIteration
should
make it clear that they are iterator objects.
1
In fact, they are iterable iterators, which
means that they can be used with
for/in
loops. The following code demonstrates just
how easy it is to write generator functions and iterate over the values they yield:
// Define a generator function for iterating over a range of integers
function range(min, max) {
for(let i = Math.ceil(min); i <= max; i++) yield i;
}
// Invoke the generator function to obtain a generator, then iterate it.
for(let n in range(3,8)) console.log(n); // Prints numbers 3 through 8.
Generator functions need never return. In fact, a canonical example is the use of a
generator to yield the Fibonacci numbers:
// A generator function that yields the Fibonacci sequence
function fibonacci() {
let x = 0, y = 1;
while(true) {
yield y;
[x,y] = [y,x+y];
}
}
// Invoke the generator function to obtain a generator.
f = fibonacci();
// Use the generator as an iterator, printing the first 10 Fibonacci numbers.
for(let i = 0; i < 10; i++) console.log(f.next());
Notice that the
fibonacci()
generator function never returns. For this reason, the gen-
erator it returns will never throw
StopIteration
. Rather than using it as an iterable
object in a
for/in
loop and looping forever, we use it as an iterator and explicitly call
its
next()
method ten times. After the code above runs, the generator
f
still retains the
execution state of the generator function. If we won’t be using it anymore, we can
release that state by calling the
close()
method of
f
:
f.close();
When you call the
close
method of a generator, the associated generator function ter-
minates as if there was a
return
statement at the location where its execution was
suspended. If this location is inside one or more
try
blocks, any
finally
clauses are run
before
close()
returns.
close()
never has a return value, but if a
finally
block raises
an exception it will propagate from the call to
close()
.
Generators are often useful for sequential processing of data—elements of a list, lines
of text, tokens from a lexer, and so on. Generators can be chained in a way that is
analogous to a Unix-style pipeline of shell commands. What is interesting about this
1.Generators are sometimes called “generator iterators” to clearly distinguish them from the generator
functions by which they are created. In this chapter, we’ll use the term “generator” to mean “generator
iterator.” In other sources, you may find the word “generator” used to refer to both generator functions
and generator iterators.
278 | Chapter 11: JavaScript Subsets and Extensions
52
approach is that it is lazy: values are “pulled” from a generator (or pipeline of genera-
tors) as needed, rather than being processed in multiple passes. Example 11-1
demonstrates.
Example 11-1. A pipeline of generators
// A generator to yield the lines of the string s one at a time.
// Note that we don't use s.split(), because that would process the entire
// string at once, allocating an array, and we want to be lazy instead.
function eachline(s) {
let p;
while((p = s.indexOf('\n')) != -1) {
yield s.substring(0,p);
s = s.substring(p+1);
}
if (s.length > 0) yield s;
}
// A generator function that yields f(x) for each element x of the iterable i
function map(i, f) {
for(let x in i) yield f(x);
}
// A generator function that yields the elements of i for which f(x) is true
function select(i, f) {
for(let x in i) {
if (f(x)) yield x;
}
}
// Start with a string of text to process
let text = " #comment \n \n hello \nworld\n quit \n unreached \n";
// Now build up a pipeline of generators to process it.
// First, break the text into lines
let lines = eachline(text);
// Next, trim whitespace from the start and end of each line
let trimmed = map(lines, function(line) { return line.trim(); });
// Finally, ignore blank lines and comments
let nonblank = select(trimmed, function(line) {
return line.length > 0 && line[0] != "#"
});
// Now pull trimmed and filtered lines from the pipeline and process them,
// stopping when we see the line "quit".
for (let line in nonblank) {
if (line === "quit") break;
console.log(line);
}
Typically generators are initialized when they are created: the values passed to the
generator function are the only input that the generator receives. It is possible, however,
to provide additional input to a running generator. Every generator has a
send()
meth-
od, which works to restart the generator like the
next()
method does. The difference
11.4 Iteration | 279
Core JavaScript
62
is that you can pass a value to
send()
, and that value becomes the value of the
yield
expression. (In most generator functions that do not accept additional input, the
yield
keyword looks like a statement. In fact, however,
yield
is an expression and has
a value.) In addition to
next()
and
send()
, another way to restart a generator is with
throw()
. If you call this method, the
yield
expression raises the argument to
throw()
as an exception. The following code demonstrates:
// A generator function that counts from an initial value.
// Use send() on the generator to specify an increment.
// Use throw("reset") on the generator to reset to the initial value.
// This is for example only; this use of throw() is bad style.
function counter(initial) {
let nextValue = initial; // Start with the initial value
while(true) {
try {
let increment = yield nextValue; // Yield a value and get increment
if (increment) // If we were sent an increment...
nextValue += increment; // ...then use it.
else nextValue++; // Otherwise increment by 1
}
catch (e) { // We get here if someone calls
if (e==="reset") // throw() on the generator
nextValue = initial;
else throw e;
}
}
}
let c = counter(10); // Create the generator at 10
console.log(c.next()); // Prints 10
console.log(c.send(2)); // Prints 12
console.log(c.throw("reset")); // Prints 10
11.4.4 Array Comprehensions
An array comprehension is another feature that JavaScript 1.7 borrowed from Python.
It is a technique for initializing the elements of an array from or based on the elements
of another array or iterable object. The syntax of array comprehensions is based on the
mathematical notation for defining the elements of a set, which means that expressions
and clauses are in different places than JavaScript programmers would expect them to
be. Be assured, however, that it doesn’t take long to get used to the unusual syntax and
appreciate the power of array comprehensions.
Here’s an array comprehension that uses the
range()
function developed above to in-
itialize an array to contain the even square numbers up to 100:
let evensquares = [x*x for (x in range(0,10)) if (x % 2 === 0)]
It is roughly equivalent to the following five lines:
let evensquares = [];
for(x in range(0,10)) {
if (x % 2 === 0)
280 | Chapter 11: JavaScript Subsets and Extensions
77
evensquares.push(x*x);
}
In general, an array comprehension looks like this:
[ expression for ( variable in object ) if ( condition ) ]
Notice that there are three main parts within the square brackets:
• A
for/in
or
for/each
loop with no body. This piece of the comprehension includes
a
variable
(or, with destructuring assignment, multiple variables) that appears to
the left of the
in
keyword, and an
object
(which may be a generator, an iterable
object, or an array, for example) to the right of the
in
. Although there is no loop
body following the object, this piece of the array comprehension does perform an
iteration and assign successive values to the specified variable. Note that neither
the
var
nor the
let
keyword is allowed before the variable name—a
let
is implicit
and the variable used in the array comprehension is not visible outside of the square
brackets and does not overwrite existing variables by the same name.
• An
if
keyword and a
conditional
expression in parentheses may appear after the
object being iterated. If present, this conditional is used to filter iterated values.
The conditional is evaluated after each value is produced by the
for
loop. If it is
false
, that value is skipped and nothing is added to the array for that value. The
if
clause is optional; if omitted, the array comprehension behaves as if
if (true)
were present.
• An
expression
that appears before the
for
keyword. This expression can be thought
of as the body of the loop. After a value is returned by the iterator and assigned to
the variable, and if that value passes the
conditional
test, this expression is eval-
uated and the resulting value is inserted into the array that is being created.
Here are some more concrete examples to clarify the syntax:
data = [2,3,4, -5]; // An array of numbers
squares = [x*x for each (x in data)]; // Square each one: [4,9,16,25]
// Now take the square root of each non-negative element
roots = [Math.sqrt(x) for each (x in data) if (x >= 0)]
// Now we'll create arrays of property names of an object
o = {a:1, b:2, f: function(){}}
let allkeys = [p for (p in o)]
let ownkeys = [p for (p in o) if (o.hasOwnProperty(p))]
let notfuncs = [k for ([k,v] in Iterator(o)) if (typeof v !== "function")]
11.4.5 Generator Expressions
In JavaScript 1.8,
2
you can replace the square brackets around an array comprehension
with parentheses to produce a generator expression. A generator expression is like an
array comprehension (the syntax within the parentheses is exactly the same as the
syntax within the square brackets), but its value is a generator object rather than an
2.Generator expressions are not supported in Rhino at the time of this writing.
11.4 Iteration | 281
Core JavaScript
53
array. The benefits of using a generator expression instead of an array comprehension
are that you get lazy evaluation—computations are performed as needed rather than
all at once—and that you can work with potentially infinite sequences. The disadvant-
age of using a generator instead of an array is that generators allow only sequential
access to their values rather than random access. Generators, that is, are not indexable
the way arrays are: to obtain the nth value, you must iterate through all n-1 values that
come before it.
Earlier in this chapter we wrote a
map()
function like this:
function map(i, f) { // A generator that yields f(x) for each element of i
for(let x in i) yield f(x);
}
Generator expressions make it unnecessary to write or use such a
map()
function. To
obtain a new generator
h
that yields f(x) for each x yielded by a generator
g
, just write
this:
let h = (f(x) for (x in g));
In fact, given the
eachline()
generator from Example 11-1, we can trim whitespace and
filter out comments and blank lines like this:
let lines = eachline(text);
let trimmed = (l.trim() for (l in lines));
let nonblank = (l for (l in trimmed) if (l.length > 0 && l[0]!='#'));
11.5 Shorthand Functions
JavaScript 1.8
3
introduces a shorthand (called “expression closures”) for writing simple
functions. If a function evaluates a single expression and returns its value, you can omit
the
return
keyword and also the curly braces around the function body, and simply
place the expression to be evaluated immediately after the argument list. Here are some
examples:
let succ = function(x) x+1, yes = function() true, no = function() false;
This is simply a convenience: functions defined in this way behave exactly like functions
defined with curly braces and the
return
keyword. This shorthand syntax is particularly
convenient when passing functions to other functions, however. For example:
// Sort an array in reverse numerical order
data.sort(function(a,b) b-a);
// Define a function that returns the sum of the squares of an array of data
let sumOfSquares = function(data)
Array.reduce(Array.map(data, function(x) x*x), function(x,y) x+y);
3.Rhino does not implement this feature at the time of this writing.
282 | Chapter 11: JavaScript Subsets and Extensions
Documents you may be interested
Documents you may be interested