On to one of the more interesting additions to JavaScript, and these are Iterators and Generators. But before diving into what they are and how to use them, let’s first examine how we would typically iterate over data using the well known for loop method. This will give us a basis for understanding the need for iterators and how powerful they can be.
Anyone who has programmed in JavaScript will have most likely written code that involves a for loop. Let’s say we have an array of friends that contains their names and we want to loop over each friend’s name and print it to the console. Using a for loop your code would typically look like this:
var friends = ['John', 'Rhoda', 'Yasmin', 'Abdi'];
for(var i = 0; i<friends.length; i++) {
console.log(friends[i])
}
// "John"
// "Rhoda"
// "Yasmin"
// "Abdi"
OK, so this is easy enough - it loops through the friends array and prints out each name one at a time. The for
loop tracks the index of the friends
array with the i
variable and the value of the i
increments each time the loop executes provided that i
is not larger than the length of the friends
array.
This is a really simple example, but loops can grow in complexity when you nest them, which means that you have to keep track of multiple variables. More often than not, this additional complexity leads to errors and bugs in your code as result because the way in which the for
loop works means that you might write similar code in multiple places. Iterators were designed to solve this problem.
In fact, many programming languages have shifted from iterating over data with for
loops to using iterator objects that programmatically return the next item in a collection. Iterators greatly improve data processing and when they are coupled with the new array methods, the new types such as sets and maps, they become an integral part of the language.
So far we have painted a rosy picture about iterators in ES6 without actually really understanding what they are and how we can use them. It’s time for us now to take a deep dive into the world of Iterators and Generators in ES6.
What are Iterators?
Iterators are nothing special, in fact they are just good old JavaScript objects with a next
method on them. The next
methods was designed for iteration, and this method returns what’s called a ‘result object’. This result object has two properties, value
and done
.
Things will be become a lot clearer when we write some code, so let’s create an Iterator function that will take a collection, i.e. an object that is iterable, and will return an object, when we call its next()
method, with the two properties that we mentioned already, value
and done
. I’m going to write it in the most simplest way, so that we can understand the underlying concept of an Iterator in ES6:
function createIterator(items) {
let i = 0;
return {
next() {
let done = (i >= items.length);
let value;
if(!done) {
value = items[i];
i++;
} else {
value = undefined;
}
//var value = !done ? items[i++] : undefined;
return {
done,
value
};
}
};
}
var iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
// for all further calls
console.log(iterator.next()); // "{ value: undefined, done: true }"
In our function createIterator
, we first declare a variable i
that we will use to keep track of the index of the items that we are iterating over, items
is passed in as a parameter when the function is called. Then we simply return an object that contains a single item, the next
function. All the next
function does is also return an object that contains two properties, value
and done
.
Inside the next
method, we delcare two variables, done
and value
. done
is a boolean and all we are doing is checking to see if our tracker variable i
, is greater than or equal to the length of the passed in iterable - we did the same thing with the for loop.
We then do a check to see if done
is false: if(!done)
if it is, we assign value to contain the item at the current index using i
and then increment i
so that it can contains the next index. Finally, the next
method returns an object containing two properties and their values are done
and value
respectively.
Each time the next()
method is called, the next value in the items array is returned as value
. When i
is 3, done
becomes true and this means that we execute the code inside of else in the next
method, which simply assigns undefined
as the value since we have iterated over all of the items of the passed in array.
Our code inside the next
method could do with a bit of refactoring, we can get rid ofthe if
else
and use a ternary operator to make an assignment to our value
variable. Here’s the refactored code:
function createIterator(items) {
let i = 0;
return {
next() {
let done = (i >= items.length);
let value = !done ? items[i++] : undefined;//refactored from the if else code block
return {
done,
value
};
}
};
}
var iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
// for all further calls
console.log(iterator.next()); // "{ value: undefined, done: true }"
Our implementation of an iterator is a really simple one, and you can see from the example that creating an Iterator function which meets all of the rules laid out in ES6 can be quite a challenge. This is where Generators come into the equation.
What are Generators?
Simply put, a Generator is a function that returns an iterator. They behave in pretty much the same way as normal functions with a few differences. Generator functions contain the * star character when they are declared after the function
keyword. Generator functions that allow you to use the yield
keyword to return iterator objects.
In terms of declaring a Generator function, it doesn’t matter where we place the star, as long as it’s the first thing after the function
keyword. So, the following Generator function declarations are all valid:
function *nameList() {};
function* nameList() {};
function * nameList() {};
Let’s create an Generator function so that we can better understand how it works:
function *createIterator() {
yield 1;
yield 2;
yield 3;
}
let iterator = createIterator();
console.log(iterator.next());// { value: 1, done: false }
console.log(iterator.next());// { value: 2, done: false }
console.log(iterator.next());// { value: 3, done: false }
Let’s take it from the top, we declared a Genrerator function using the star character, and we are using the the yield
keyword, which is new in ES6, and specifies values the resulting itreator should return when next()
is called and the order they should be returned in. As you can see from the code example, we are calling the createIterator
function and assigning the return value, which is an object, to the iterator
variable.
At this moment we have an object available to us with a next()
method that we can call. Once we call the next
method, we get back an object with two properties value
and done
- just like we did previously when we created our own iterator. The iterator generated in this example has three different values to return on successive calls to the next() method: first 1, then 2, and finally 3. A generator gets called like any other function, when we call the next
method on that is available to us on the iterator
variable.
Each call to next()
returns the hard coded value that we put in the Generator function, in our example, use the yield
keyword to return 3 times - and remember the value is only returned after you call next()
and we will see why in just a moment.
I mentioned previously that Generator functions behaved similarly to normal functions, but that they have some unique behavior. We have already seen that they are delcared in a different way to normal functions, so when we are creating a Generator function we need to include the star charater after the function
keyword.
Generator functions behave differently to normal functions when they are called. Generator functions stop execution after the yield
statement. So in the other words, when the yield
statement is run, execution is halted until next()
is called again. When next()
is called again yield 2
is executed and once again execution is stopped until next()
is called again, then yield 3
is run and so on.
All subsequent calls to next()
would return value
as undefined
and done
would contain true
. The resulting behavior is very similar to the one we got from our iterator function that we implemented previously. Both returned an object once we called the function initially, and when we called the next
method they returned each value and wether we were done iterating over all available items.
The main difference is that we didn’t have to write the functionality ourselves, rather all we had to do was create the special Generator function and simply use the yield
keyword to return a value when next()
was called.
The yield
keyword can be used with any value or expression, so this means that you can create Generator functions that add items to iterators without just listing them one by one, like we did in our example. You can even use yield
inside a for
loop:
function *createIterator(items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
}
let iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
// for all further calls
console.log(iterator.next()); // "{ value: undefined, done: true }"
The above example has simply refactored the use of the keyword yield
to be computed from a passed in array. When we call our Generator function createIterator
we pass it an array of items as a parameter and this array is then iterated over using a for
loop. Inside the body of the loop we yield the elements from the array into the iterator as the loop progresses. Just as before, each time yield
is encountered the loop stops and picks up from the same position when next()
is called on the iterator.
yield
can only be used inside of Generator functions
As you might have gathered the yield
keyword can only be used inside of Generator functions. If you use this keyword anywhere else beside a generator function, then this will create a syntax error. This also applies to functions that are inside of Generator functions themselves. For example, the following will generate a syntax error:
function *createIterator(items) {
items.forEach(function(item) {
// syntax error
yield item + 1;
});
}
In this example, yield
is actually insidethe Generator function but still create a syntax error. This is because yield
in the inner function, cannot cross function boundaries - just like a nested function cannot return a value for its containing function.
Generator Function Expressions
Just like creating normal function expressions, you can also create Generator function expressions. To create a Generator function expression, you simply add the star charatcter between the function keyword and the openning parenthesis. Let’s refactor the previous example of the generator function and turn into a Generator function expression:
let createIterator = function *(items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
}
let iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: 3, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
// for all further calls
console.log(iterator.next()); // "{ value: undefined, done: true }"
We simply refactored the previous Generator function declaration and turned it into a function expression. Because the function is anonymous, the asterisk needs to go the functio
keyword and openning parenthesis. It’s also worth noting that you cannot create Generator function using the fat arrow syntax.
Generator Object Methods
As I mentioned previously, Generator functions behave just like normal functions with some differences. So, just like you can add add a function to an object as a method, you can also add a Generator function as a method on an object - here is how we would add a function as a Generator in ES6:
let myObj = {
*createIterator(items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
}
};
let iterator = myObj.createIterator([1, 2, 3]);
In the shorthand version of adding methods to objects in ES6 you ommit the function keyword, this means that when creating a Generator function you place the star immediately before the method name. You can leave blank space between the method name and the star, as the whitespace will simply be ignored.
Iterables and for-of & accessing the Default Iterator
As we’ve already seen in the section on Arrays, Sets, and Maps, we can use the for..of loop to iterate over these objects. An iterable is an object with a Symbol.iterator
property. So in other words, all iterable objects in ES6 have this property on them. This is actually a method that returns an iterator for the iterable object.As we’ve already seen in previous sections, we can use the for loop with an iterable.
When using a normal for loop, we have to keep track of the index, however when we used iterators we no longer had this issue. Now when we use for of on an iterable it removes the need for us to track an index when iterating over an iterable all together. So this means that we don’t need to keep track of anything with the for of loop, and we simply have available to us the contents of each item in a collection.
Now that we have a better understanding of how iterators work, we can now look at how the for of loop works under the hood and how it uses an iterator when iterating over a collection. At its core, the for of loop uses the Symbol.iterator
property on the iterable object. In other words, it simply calls this function and gets the iterable object which has the next()
method available on it.
Say we have an array of fruits, we can loop over this array using the for of loop since arrays are iterables:
let fruits = ['apple', 'grapes', 'banana'];
for(let fruit of fruits) {
console.log(fruit);
}}
//apple
//grapes
//banana
The for of loop reads each element directly from the array and assigns it to the named variable, which is fruit
. But how does this actually work? As I mentioned the for loop simply makes use of the iterator returned from calling the arrays’ Symbol.iterator
function and this returns an iterator that the for of loop can use.
Since the iterator know how to access items from a collection one at a time, all the for loop needs to do is call the next()
method then save the value into a variable - in our example we specified fruit
. So what is really happening behind the scenes is that the for of loop simply calls the Symboliterator
function:
let iterator = fruits[Symbol.iterator]();
It then calls iterator.next().value
and assigns this to the variable fruit
, it keeps on doing this until next().done
is equal to true.
let iterator = fruits[Symbol.iterator]();
let fruit = iterator.next().value;
So essentially it’s calling next().value
and then assigning this to the fruit variable that we provided to it. We can actually loop over the array using a while
loop by using the iterator returned by calling fruits[Symbol.iterator]()
:
let fruits = ['apple', 'grapes', 'banana'];
let iterator = fruits[Symbol.iterator]();
let counter = 0;
while (counter < fruits.length) {
let fruit = iterator.next().value;
console.log(fruit);
counter++;
}
//apple
//grapes
//banana
Using the for of loop is should be used where possible, as it’s less error prone and yo don’t have to keep track of the index in a sequence. We shouldn’t abandon the for loop altogether, but it’s use should be reserved for complex scenarios where you need more control. The for of loop only works on iterable objects, so this means that when you try to use the for loop with a non iterable object such as plain JavaScript object {}
, null
or undefined
it will throw an error: TypeError: [Symbol.iterator] is not a function
.
Creating Iterables
As I mentioned, calling the for of loop on a plain JavaScript object will throw a type error, as there is no Symbol.iterator
property on the object. Since this is not built-in, we can actually add a the Symbol.iterator
property ourselves. Let’s say we have a simple object that contains an array of items as a single property:
let simpleObj = {
items: ['Milk', 'ES6', 'Gym']
};
So this is a typical object nothing special. However, we can’t call the for of on it. We simply add the Symbol.iterator
as a property method:
let simpleObj = {
items: ['Milk', 'ES6', 'Gym'],
*[Symbol.iterator]() {
for (let item of this.items) {
yield item;
}
}
};
for (let item of simpleObj) {
console.log(item);
}
//output
//Milk
//ES6
//Gym
All we’ve done is add another property, which is actually a Generator function. It is now possible to use the for of loop on our object since it has the Symbol.iterator
property. The for of will use this as the default operator and when it calls the function it gets back the iterator. From here, all it does is call the iterator.next()
and assigns the value to our variable prop
and then this logged on to the console. This is repeated until iterator.next().done
is equal to true - hence our output Milk, ES6, Gym.
The Generator that we created uses a for of loop on each of the elements of the items array, remember that arrays are iterable by default and this means we can use the for of loop on them. Then it uses the yield
keyword to return each element in the items array.
The iterator that we added to our object merely returns each item in the items array, using the arrays default iterator. But what if we want our object to behave just like the array, so when we call for of loop on it, all of the objects properties values are retireived one at a time using the for of loop. We can easily do that all we have to do is refactor the Generator function to yield each property on every call to next()
:
let simpleObj = {
items: ['Milk', 'ES6', 'Gym'],
anotherProp: 'Hello World!',
Name: 'Abdi',
*[Symbol.iterator]() {
let keys = Object.keys(this)
for(let i =0;i<keys.length;i++) {
yield this[keys[i]]
}
}
};
for (let item of simpleObj) {
console.log(item);
}
//output:
// ["Milk", "ES6", "Gym"]
// Hello World!
// Abdi
Now our object behaves just like an iterable object, all we did is use a for loop to return each property in the object via the yield statement. In most cases you won’t need to create your own iterators, as ES6 comes with many built-in iterators that you can use to efficiently work with collections in JavaScript
Built-in Iterators
Only in certain special use cases would you need to create your own iterators, ES6 comes with iterators by default and we have already seen some these in action. In ES6 there are 3 types of collection objects: Arrays, Maps and Sets and by default all three types of collections have the following built-in iterators to help you retrieve their content:
- entries()
- values()
- keys()
Entries iterator
The entries()
iterator returns a two item as the value every time next()
is called. The two items represent the key and the value for each item in the collection. For arrays, the first item in the pair is the numeric index and the second item second item is the actual element on that index. For Sets the first item is also the numeric index, but since the values double up as the numeric index this means that it will return both key and value as the same. And finally, for Maps the first item is the key and the second is the value.
Let’s look at an example of each type of collection with the entries iterator:
let colors = [ "red", "green", "blue" ];
let tracking = new Set(['Milk', 'ES6', 'Gym']);
let data = new Map();
data.set("Name", "Abdi Cagarweyne");
data.set("Occupation", "Web Developer");
for (let entry of colors.entries()) {
console.log(entry);
}
for (let entry of tracking.entries()) {
console.log(entry);
}
for (let entry of data.entries()) {
console.log(entry);
}
// output
// [0, "red"]
// [1, "green"]
// [2, "blue"]
// ["Milk", "Milk"]
// ["ES6", "ES6"]
// ["Gym", "Gym"]
// ["Name", "Abdi Cagarweyne"]
// ["Occupation", "Web Developer"]
Values iterator
The values()
iterator returns each value associated with an item in a collection. We will simply use the values()
iterator in each for loop:
let colors = [ "red", "green", "blue" ];
let tracking = new Set(['Milk', 'ES6', 'Gym']);
let data = new Map();
data.set("Name", "Abdi Cagarweyne");
data.set("Occupation", "Web Developer");
for (let value of colors.values()) {
console.log(entry);
}
for (let value of tracking.values()) {
console.log(entry);
}
for (let value of data.values()) {
console.log(entry);
}
// output
// "red"
// "green"
// "blue"
// "Milk"
// "ES6"
// "Gym"
// "Abdi Cagarweyne"
// "Web Developer"
Keys iterator
The keys iterator returns each key associated with a collection:
let colors = [ "red", "green", "blue" ];
let tracking = new Set(['Milk', 'ES6', 'Gym']);
let data = new Map();
data.set("Name", "Abdi Cagarweyne");
data.set("Occupation", "Web Developer");
for (let key of colors.keys()) {
console.log(key);
}
for (let key of tracking.keys()) {
console.log(key);
}
for (let key of data.keys()) {
console.log(key);
}
// output
// 0
// 1
// 2
// Milk
// ES6
// Gym
// Name
// Occupation
The Spread Operator and Non-Array Iterables
The spread operator, as we’ve already seen, works by spreading out the contents of an array into individual values. So, you will recall this example from the spread section previously:
let numbers = [1, 2, 3];
function sum(num1, num2, num3) {
return num1 + num2 + num3;
}
sum(...numbers);
// ouput
//6
What I didn’t mentioned was that the spread operator also works on any iterable object, and works by using the default iterator to determine which values to include. Let’s look at an example where we spread out the values from a Set into an array:
let set = new Set([1, 2, 3, 3, 3, 4, 5];
let array = [...set];
console.log(array);
// output
// [1,2,3,4,5]
This works on Sets because the spread operator uses the default iterator and in the case of Arrays and Sets their default iterator is the values()
iterator. This means that what is returned on each call to next using this iterator is the value for the Array and the Sets. As I mentioned the spread operator works on any iterable, so this means that it will also work on Maps, here is an example that demonstrates that:
let person = new Map([['name', 'Abdi'], ['job title', 'JS Developer']]);
let array = [...person]
console.log(array)
// output
// [["name", "Abdi"], ["job title", "JS Developer"]]
In the example above we create a new Map object and initialize it with two properies, name
and job title
. We can initialize the Map when we call the constructor initially and pass in an array of key value pairs that we want the Map to contain. So, in this example we pass in an array that contains two arrays, the first array contains two elements: name and abdi, this will be used as the key value pair. The we have another array that contains: job title and JS Developer, similarly these will be used to populate the second key value pair in our map.
The default iterator for the Map object is the entries()
iterator, and this means that it returns key-value pairs on each call to next()
. This is why we have an array whose first and second elements are arrays. The really cool thing about the spread operator is you can use it in an array literal as many times as you want, and you can use it wherever you want to insert multiple items from an iterable.
For example let’s say that you have three arrays and each contain single string elements and you want to combine them to create one array that contains all of the contents aof the previous three arrays, you can do this using the spread operator easily:
let smallNumbers = [1, 2, 3];
let mediumNumbers = [4, 5, 6];
let largeNumbers = [7, 8, 9];
let allNumbers = [0, ...smallNumbers, ...mediumNumbers, ...largeNumbers, 10];
console.log(allNumbers);
// output
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
We’ve used the spread operator to create allNumbers
, the first element in the array we’ve specified as 0 and the last 10. In between these two elements the spread operator was used to read the values from each array in the order that they appear in their respective arrays. The original values are changed all that has happened is that their values have been copied into the allNumbers array using the spread operator. Using the spread operator is the easiest way convert an iterable oject into an array.