ES6 comes with lots of new cool features. Out of which, Iterators and Generators took some time to get used to. Here's an example that shows how to use ES6 Iterators and Generators.
Since this is experimental stuff, you need latest version of Chrome/Firefox to run the code. I used Chrome's Dev tool snippets to author/run the given example code.
Let's start with the problem definition.
Assume that you have a business class Basket
that used to hold a list of items and other related methods. Now you need a way to iterate through all the items inside the Basket
. Since Basket
is a custom class, you can not use JavaScript's for..in
. You need to write your own custom logic that iterates, as shown below.
Iterating Basket
using for
loop
'use strict';
function Basket() {
// array to hold things inside the basket
this.container = [];
}
// business method add that puts the given item in the basket
Basket.prototype.add = function(item) {
if (!!item) {
this.container.push(item);
}
}
function test() {
// create new instance of Basket.
var fruitBasket = new Basket();
// put fruits in the fruitBasket
fruitBasket.add('apple');
fruitBasket.add('banana');
fruitBasket.add('orange');
// list all fruits in the fruitBasket
for(let index=0; index < fruitBasket.container.length; index++){
console.log(fruitBasket.container[index]);
}
};
test();
Problems with for
loop approach
- The
for
loop inside thetest
method is accessing a propertycontainer
which is supposed to be internal to thefruitBasket
object. There is no need for the outer world to know the internal of an object to iterate it. Of course, there are other ways to fix it, but for this article's purpose, lets not go there. - You can't use
for(let fruit in fruitBasket)
to iterate through the items. Because, althogh semanticallyfruitBasket
is a list, technically it is not a list or an array. So, you can't use the elegant way offor..each
. So sad!
Let's fix this with the use of Iterators.
Enter Iterator
Given the above problem, we need a way to tell JavaScript that our custom object is indeed a list
and let it use for..each
in our object. How do we do it? With the help of Iterator
, of course. We need to define a method in our object that JavaScript will invoke when it wants to iterate our object. And, what method will that be?
If you are coming from Java background, you must be aware of Interface
. If an object tends to implement an interface, then the Java run time knows for sure that the object will have an implementation of all methods defined in that interface. So, without fear, it will invoke that method.
But, JavaScript is not Java and we don't have Interfaces here. What else we have? ES6.
ES6 defines two new protocols iterator
and iterable
, which in layman terms means that, you can define custom iterator methods in your object and tell JavaScript that that's our iterator method.
For example, you can say,
var myObj = {};
myObj[Symbol.iterator] = function () {
return {
next: function() {
return {
value: 'anything',
done: false // or true if completed iterating
}
}
}
}
and you can use,
for(let thing of myObj) { console.log(thing); }
All you need to do is from your custom iterator function, return an object that has a method next()
defined which inturn will return an object in the format of {value: 'val', done: false}
. JavaScript will use the well-known Symbol.iterator
to identify if an object is iterable and has an iterator function defined in it. (More about Symbol.iterator)
How sweet! Let's use this concept in our Basket
problem.
'use strict';
function Basket() {
this.container = [];
// maintain the state of current index value while iterating
this._index = 0;
}
Basket.prototype.add = function(item) {
if (!!item) {
this.container.push(item);
}
}
// define custom iterator
Basket.prototype[Symbol.iterator] = function() {
return {
next: function() {
var result;
if (this._index < this.container.length) {
result = {
value: this.container[this._index],
done: false
};
this._index++;
} else {
// completed iterating through all items.
result = {
done: true
}
this._index = 0;
}
return result;
}.bind(this)
};
}
function test() {
var fruitBasket;
fruitBasket = new Basket();
fruitBasket.add('apple');
fruitBasket.add('banana');
fruitBasket.add('orange');
for (let fruit of fruitBasket) {
console.log('fruit fetched through iterator is ', fruit);
}
};
test();
Now our code has improved a bit. From our fruitBasket
, we are not exposing the internals to the outer world to iterate. And we get to use for..of
to iterate fruitBasket
.
But still, there's a problem. If you look at the code inside our custom iterator, you will notice that we are maintaining the state manually using the variable _index
. We manually update this value as we iterate through the list. Is there an alternative to this?
Yes and it's called as Generator functions.
Enter Generator
By definition, A generator is a special type of function that works as a factory for iterators. In English, a generator in a function returns a {value: 'val', done: false}
if there are items left and returns {done:true}
if there are no items. And, it maintains internal state by its own.
A Generator function should have,
function* ()
syntax- One or more
yield
expressions inside the function body.yield
is a special keyword that will return a value in the form of{value: 'val', done: false}
. (More about Yield)
Let's put this together in our Basket
problem.
'use strict';
function Basket() {
this.container = [];
}
Basket.prototype.add = function(item) {
if (!!item) {
this.container.push(item);
}
}
Basket.prototype[Symbol.iterator] = function*() {
for(let index=0; index < this.container.length; index++){
yield this.container[index];
}
}
function test() {
var fruitBasket, i;
fruitBasket = new Basket();
fruitBasket.add('apple');
fruitBasket.add('banana');
fruitBasket.add('orange');
for(let fruit of fruitBasket){
console.log('fruit fetched through iterator/generator is', fruit);
}
};
test();
Notice how simple our iterator method has become.
Hopefully, this article sheds some light on the ES6 Iterator / Generator concepts. Please leave your questions / feedback (if any) in the comment section.
undefined