Modified June 17, 2016.
Generators and the spread operator in JavaScript are probably two of my favorite additions, just behind let
, const
, and arrow functions. It allows for richer functions to be constructed. Here’s an overview/tutorial of what it is, and how it can be used.
Generators, if you are not already familiar, are a special type of function that are able to sequentially yield the outputs of a function. The way JavaScript generators work is that there is a main generator object, from which you can create GeneratorFunction
instances. You can obtain a single step of this instance using the next
method, which returns an object containing two key-value pairs, value
and done
. The value
key is what is yielded by the generator, and done
is a boolean saying whether or not the generator has anything left to yield. Generators are made using the function*
and yield
keywords. I’ll show you a simple generator which generates 1
, then 2
, then 3
, then stops.
function* threeNumbers(){
yield 1;
yield 2;
yield 3;
}
To use this generator, assign the result of calling threeNumbers
to a variable, then use it’s method next
, like so:
let a = threeNumbers(); // note that we're not using `new` here
a.next(); // {value: 1, done: false}
a.next(); // {value: 2, done: false}
a.next(); // {value: 3, done: false}
a.next(); // {value: undefined, done: true}
This is a trivial generator, and can hardly be said to do anything useful. It is possible to create an infinite generator–any valid JS code can be used inside a generator. Each time the generator encounters yield
it will pause what it is doing and return a value. Once next
is called again, it will resume.
Generators can also take arguments. The arguments taken by generators become part of each GeneratorFunction
s scope. To show this, let’s look at a generator that generates multiples of a given number.
function* multiplesOf(num){
let counter = 0;
while(true){
yield counter;
counter += num;
}
}
let of3 = multiplesOf(3);
of3.next().value; // 0
of3.next().value; // 3
of3.next().value; // 6
of3.next().value; // 9
// ... etc.
Symbol.iterator
While generators may be interesting, you are probably wondering how they are practical. Enter Symbol.iterator
. An objects Symbol.iterator
dictates how it behaves in for..of
loops. Here’s a simple Collection
class. (Note that *name(){...}
in a class statement is a generator!)
class Collection {
constructor(){
this.vals = [];
}
get size(){
return this.vals.length;
}
retrieve(ind){
return this.vals[ind];
}
add(val){
this.vals.push(val);
}
*[Symbol.iterator](){
for(let i = 0; i < this.size; i++){
yield this.retrieve(i);
}
}
}
let a = new Collection();
a.add(5);
a.add(3);
a.add("Hi!");
for(let p of a){
console.log(p);
}
// logs:
// 5
// 3
// Hi!
An iterable object is an object for which it’s Symbol.iterator
is well-defined, as above. Simple! Not only is the Symbol.iterator
used for for..of
loops, it’s also used with the spread operator. The spread operator applies the yield of the iterable to a function, array, etc. For example, a string is iterable. Observe:
for(let i of "Hi!"){
console.log(i);
}
// logs:
// H
// i
// !
Thus, we can apply its members to an array, or to a function:
console.log([..."Hi!"]); // ["H", "i", "!"]
let f = (x, y, z) => y + z + x;
console.log(f(..."Hi!")); // "i!H"
What I love about the spread operator is that you can use it as a function argument operator! When used in this way, it’s called the “rest paramter”.
// returns it's arguments as an array
// That is, `a` contains all the arguments of the function
let f = (...a) => a;
// `b` always contains the first argument, and `a` contains the rest
// `a` is an empty array when only one argument is given.
let g = (b, ...a) => [b, a];
// however, functions like (...a, k) are not valid -- the rest parameter
// can only be used as the last argument to a function.
Say you have an array A
and a function f
. Here is a chart dictating some helpful results.
f = (a) => a; f(A) equals A
f = (...a) => a; f(A) equals [A]
f = (a) => a; f(...A) equals A[0]
f = (...a) => a; f(...A) equals A
While I like the rest paramter, I feel as if it could’ve been defined better. For example, there is little ambiguity when I write (...a, b)
–I refer to an array a
containing all but the last argument, which then is b
. The question then is what the behaviour would be when only a single argument is passed. I would say that a
should contain a single member, and let b
be undefined, though I’m sure there would be contention on this subject.