Iterators and generators are usually a secondary thought when writing code, but if you can take a few minutes to think about how to use them to simplify your code, they'll save you a lot of debugging and complexity. With the new ES6 iterators and generators, JavaScript gets similar functionality to Java's Iterable, allowing us to customize our iteration on objects.
For example, if you had a Graph object, you can easily use a generator to walk through the nodes or edges. This makes for much cleaner code by putting the traversal logic within the Graph object where it belongs. This separation of logic is good practice, and iterators/generators make it easier to follow these best practices.
ES6 Iterators and Generators
Iterators
Using iterators, you can create a way to iterate using the for...of
construct for your custom object. Instead of using for...in
, which just iterates through all of the object's properties, using for...of
lets us make a much more custom and structured iterator in which we choose which values to return for each iteration.
Under the hood, for...of
is actually using Symbol.iterator
. Recall Symbols are a new feature in JavaScript ES6. The Symbol.iterator
is special-purpose symbol made especially for accessing an object's internal iterator. So, you could use it to retrieve a function that iterates over an array object, like so:
var nums = [6, 7, 8];
var iterator = nums[Symbol.iterator]();
iterator.next(); // Returns { value: 6, done: false }
iterator.next(); // Returns { value: 7, done: false }
iterator.next(); // Returns { value: 8, done: false }
iterator.next(); // Returns { value: undefined, done: true }
When you use the for...of
construct, this is actually what is being used underneath. Notice how each subsequent value is returned, along with an indicator telling you if you're at the The vast majority of the time you won't need to use next()
manually like this, but the option is there in case you have a use case requiring more complex looping.
You can use Symbol.iterator
to define specialized iteration for an object. So lets say you have your own object that's a wrapper for sentences, Sentence
.
function Sentence(str) {
this._str = str;
}
To define how we iterate over the internals of the Sentence object, we supply a prototype iterator function:
Sentence.prototype[Symbol.iterator] = function() {
var re = /\S+/g;
var str = this._str;
return {
next: function() {
var match = re.exec(str);
if (match) {
return {value: match[0], done: false};
}
return {value: undefined, done: true};
}
}
};
Now, using the iterator we just created above (containing a regex string that matches only words), we can easily iterate over the words of any sentence we supply:
var s = new Sentence('Good day, kind sir.');
for (var w of s) {
console.log(w);
}
// Prints:
// Good
// day,
// kind
// sir.
Generators
ES6 generators build on top of the what iterators supply by using special syntax for more easily creating the iteration function. Generators are defined using the function*
keyword. Within a function*
, you can repeatedly return values using yield
. The yield
keyword is used in generator functions to pause execution and return a value. It can be thought of as a generator-based version of the return
keyword. In the next iteration, execution will resume at the last point that yield
was used.
function* myGenerator() {
yield 'foo';
yield 'bar';
yield 'baz';
}
myGenerator.next(); // Returns {value: 'foo', done: false}
myGenerator.next(); // Returns {value: 'bar', done: false}
myGenerator.next(); // Returns {value: 'baz', done: false}
myGenerator.next(); // Returns {value: undefined, done: true}
Or, you could use the for...of
construct:
for (var n of myGenerator()) {
console.log(n);
}
// Prints
// foo
// bar
// baz
In this case, we can see that the Generator then takes care of returning {value: val, done: bool}
object for you, which is all handled under the hood in for...of
.
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
So how can we use generators to our advantage? Going back to our previous example, we can simplify the Sentence
iterator to the following code:
Sentence.prototype[Symbol.iterator] = function*() {
var re = /\S+/g;
var str = this._str;
var match;
while (match = re.exec(str)) {
yield match[0];
}
};
Notice how the iterator (now a Generator) function is much smaller than the previous version. We no longer need to return an object with the next
function, and we no longer need to deal with returning the {value: val, done: bool}
object. While these savings may seem minimal in this example, its usefulness will easily be realized as your Generators grow in complexity.
Advantages
As Jake Archibald points out, some advantages of these generators are:
-
Laziness: The values aren't calculated ahead of time, so if you don't iterate until the end you won't have wasted the time calculating the unused values.
-
Infinite: Since values aren't calculated ahead of time, you can have an infinite set of values be returned. Just make sure you break out of the loop at some point.
-
String iteration: Thanks to
Symbol.iterator
, String now has its own iterator to make looping over characters much easier. Iterating over the character symbols of a string can be a real pain. This is especially useful now that JavaScript ES5 supports unicode.for (var symbol of string) {
console.log(symbol);
}
Conclusion
While iterators and generators aren't huge additional features, they do help quite a bit to clean up code and to keep it organized. Keeping iteration logic with the object it belongs to is good practice, which seems to be much of the focus of ES6 features. The standard seems to be moving towards the structure-ability and design-friendliness of Java, while still maintaining the speed of development of dynamic languages.
What do you think of the new ES6 features? What kind of interesting use cases do you have for iterators and generators? Let us know in the comments!
Thanks to Jake Archibald for the great article detailing much of how iterators and generators work in ES6.