Understanding Loops Drawbacks and Iteration Protocols in JavaScript





5.00/5 (4 votes)
Learning the drawbacks of different loop constructs and learn how to use the different iteration protocols
Introduction
If you have been programming with JavaScript or any language for a while now, for
-loops shouldn’t be alien to you. Haven’t you noticed that many programming-languages, including JavaScript, have shifted from iterating data using for
-loops to using iterator-objects that return the next item of a given collection. Moreover, iterators made our lives easier and productive when working with collections. That’s why understanding JavaScript iterators are beneficial to every developer and will explore this subject in this article. OK, then let’s get started.
Background
Before you begin, it is advisable to read the JavaScript Symbols
, you can check out my post here: Basics of JavaScript Symbols. One reason is that we may come across the usage of Symbol.iterator
. Just a note, JavaScript iterators won’t be replacing for
-loops and/or other loop constructs. However, iterators' main goal is to eliminate the complexity and error-prone nature of loops.
Table of Contents
The Different Loops Construct Drawbacks
For-loop
For
-loops are one of the quintessential in learning any programming language including JavaScript. In general working with arrays, you’ll typically use a for
-loop to iterate over the data elements.
Let's see an example below:
/*simple loop*/
let dogsName = ['Tank', 'Diesel', 'Cooper', 'Bruno', 'Thor'];
for (let index = 0; index < dogsName.length; index++) {
console.log(dogsName[index]);
}
/*end of simple loop*/
I know, the code sample is quite straightforward. How about creating a complex one?
/*complex loop*/
let x = 100;
for (let i = 0; i <= x; i++) {
for (let j = 1; (j + x / 2) < x; j++) {
for (let k = 1; k <= x; k++) {
console.log("hello");
}
}
}
/*end of complex loop*/
Complexity grows, when nesting loops which can result in tracking multiple variables inside the loops. Thus, this can make your loop error-prone.
Array.forEach
However, why use for
-loop if you are just going to iterate over the data items, right? Array.forEach
to the rescue, not quite. Why? Before we answer that, let's see an example first.
/*forEach loop*/
let amounts = [1.25, 2.25, 3.25, 4.25];
/**
* Outputs:
* 1.25
* 2.25
* 3.25
* 4.25
*/
amounts.forEach((item) => {
console.log(item);
});
/*end of forEach loop*/
Now, going back to the question. Because using the Array.forEach
loop doesn't give you the ability to break within the loop construct.
Let's see an example below:
/*forEach loop*/
let amounts = [1.25, 2.25, 3.25, 4.25];
amounts.forEach((item) => {
if (item === 3.25) {
break; //throws an error: "Uncaught SyntaxError: Illegal break statement"
}
});
/*end of forEach loop*/
For-in loop
How about when you are interested only in getting those properties of an object? The for
-in
loop comes to the rescue.
Let's see an example below.
/** Start of for-in loop */
let customer = { firstName: "Jin Vincent", lastName: "Necesario", country: "Philippines" };
/**
* Output:
* firstName
* lastName
* country
*/
for (let key in customer) {
console.log(key);
}
/** End of for-in loop */
However, the for
-in
loop can only iterate over enumerable, non-Symbol properties. Thus, it doesn't make any sense to iterate over an Array
where the index order is significant.
Let's see an example below.
/** Start of for-in loop that iterates over an Array */
/** Warning: Please don't do this on your project/product! */
Array.prototype.bar = 1;
let products = [1001, "mouse", "monitor", { firstName: "Jin Vincent" }];
/**Output:
* 0
* 1
* 2
* 3
* bar
*/
for (let prop in products) {
console.log(prop); //this outputs the index of the array and bar
}
/** End of for-in loop that iterates over an Array*/
For-of loop
If you want to iterate over an array or a collection and you don't mind the index, for
-of
loop saves the day.
You can use the for
-of
loop with objects or collections that implement the iterable protocol.
More on the iterable protocol in the latter part of the article.
Let's see an example below.
/** Start of for of loop */
const xmenGoldTeam = ['Storm', 'Bishop', 'Colossus', 'Jean Grey', 'Iceman', 'Archangel'];
/**Outputs:
* Storm
* Bishop
* Colossus
* Jean Grey
* Iceman
* Archangel
*/
for (let xmen of xmenGoldTeam) {
console.log(xmen);
}
/** end of for of loop */
When an object isn't iterable, therefore, you can't use the for
-of
loop to iterate over the properties.
Let's see an example below:
/** Start of for-of loop */
let customers = { firstName: "Jin Vincent", lastName: "Necesario", country: "Philippines" };
for (const customer of customers) {
//TypeError: customers is not iterable
}
/** End of for-of loop */
Iteration Protocol
Basically, the iteration protocol is a set of rules that an object needs to follow when implementing an interface. In other words, when using a certain protocol (it could be iterable or iterator), your object should implement the following functionality and with some kind of rules.
Types of Iteration Protocol
- Iterator protocol
- Iterable protocol
What Is an Iterator Protocol?
Implementing the iterator protocol, you should follow the following conventions:
- An object should implement
.next()
method. - The
.next()
method must return an object that contains the following properties:value
anddone
.
In short, an object is an iterator when it provides a next()
method that returns the next item in the sequence of a group of items that has two properties.
These two properties are done and value.
done
- a boolean type that indicates whether or not there are any existing elements that could be iterated uponvalue
- the current element
Let's see an example below and follow the conventions.
/*Start of an iterator protocol*/
const countries = {
collection: ["Philippines", "Singapore", "Malaysia", "Canada", "Brazil", "Australia"],
index: 0,
next: function () {
if (this.index < this.collection.length) {
return {
value: this.collection[this.index++],
done: false
}
} else {
return {
done:true
}
}
}
}
console.log(countries.next()); //output: {value: "Philippines", done: false}
console.log(countries.next()); //output: {value: "Singapore", done: false}
console.log(countries.next()); //output: {value: "Malaysia", done: false}
console.log(countries.next()); //output: {value: "Canada", done: false}
console.log(countries.next()); //output: {value: "Brazil", done: false}
console.log(countries.next()); //output: {value: "Australia", done: false}
console.log(countries.next()); //output: {done: true}
/*End of an iterator protocol*/
What is an Iterable Protocol?
Before starting, one thing to point out, JavaScript does have well-known symbols which are usually prefixed using the @@
notation. Like the Symbol.iterator
, the symbol is referred to as the @@iterator
.
Now going back to iterable protocol. When an object implements the @@iterator
method are considered iterable. In addition, this method returns the iterator. The object's property @@iterator
key which is available via Symbol.iterator
.
Let's see an example to understand the statement above.
/*Start of an iterable protocol*/
const countries = {
countryCollection:
["Philippines", "Singapore", "Malaysia", "Canada", "Brazil", "Australia"],
startIndex: 0,
[Symbol.iterator]: function () {
return {
collection: this.countryCollection,
index: this.startIndex,
next: function () {
if (this.index < this.collection.length) {
return {
value: this.collection[this.index++],
done: false
}
} else {
return {
done: true
}
}
}
}
}
}
let countryIterator = countries[Symbol.iterator]();
console.log(countryIterator.next()); //output: {value: "Philippines", done: false}
console.log(countryIterator.next()); //output: {value: "Singapore", done: false}
console.log(countryIterator.next()); //output: {value: "Malaysia", done: false}
console.log(countryIterator.next()); //output: {value: "Canada", done: false}
console.log(countryIterator.next()); //output: {value: "Brazil", done: false}
console.log(countryIterator.next()); //output: {value: "Australia", done: false}
console.log(countryIterator.next()); //output: {done: true}
/**
* Outputs:
* Philippines
* Singapore
* Malaysia
* Canada
* Brazil
* Australia
* undefined
*/
for (country of countries) {
console.log(country)
}
/*End of an iterable protocol*/
Now that we have an idea of what is an iterator and iterable protocol are and where the for
-of
loop comes into play. Why not check and learn the iterable built-in types?
Iterator Built-in Types
The JavaScript language has built-in iterable types but we won't cover everything but we are going to cover String
, Array
, and Set
.
For the complete list of the iterable built-in types, here they are:
String
Array
Map
Set
TypedArray
- Arguments Object
Check if Object/type Implements Symbol.iterator
/** Start of how to check if a type/object implements the Symbol.iterator */
let programmingLanguage = "JavaScript";
//let's us check if the built-in data-type String implements Symbol.iterator
//output: [Symbol(Symbol.iterator)]
Object.getOwnPropertySymbols(programmingLanguage.__proto__);
/** end of how to check if type/object implements the Symbol.iterator */
As you can see, the code sample above shows that the String
data-type does implement the @@iterator
. Thus, Strings
are iterable.
How Does a Built-in Iterator Look Like?
Let's go straight to an example to see how does an iterator looks like.
//declare a JavaScript string type
let programmingLanguage = "JavaScript";
//lets invoke the method having an @@iterator property key
let iterator = programmingLanguage[Symbol.iterator]();
//let's check if the @@iterator property function return an iterator
console.log(iterator); //output: StringIterator
As you can see, with the example above, we have declared a String
type and invokes the @@iterator
key symbol and checks whether the function really returns an iterator and it does return a StringIterator
.
Quick Examples of Iterator Built-in Types
We won't be going into detail because we have already discussed the concepts above. However; the objective here is to show how we can interact with the built-in iterators.
Array Iterator
The @@iterator
method is part of the iterable protocol, that defines how to iterate over the items.
const pets = ["Andy", "Barkley", "Zandro"];
const petIterator = pets[Symbol.iterator]();
console.log(...petIterator); //outputs: Andy Barkley Zandro
String Iterator
The [@@iterator]()
method returns an iterator that iterates the String
values.
const fullName = "Jin Vincent Necesario";
const fullNameIterator = fullName[Symbol.iterator]();
console.log(...fullNameIterator); //outputs: J i n V i n c e n t N e c e s a r i o
Set Iterator
Set.prototype[@@iterator]()
The initial value of the @@iterator
is the same as the initial value of the values
property.
const myArraySet = new Set(['mango', 'avocado', 'apple', 'apple', 'mango']);
const myArraySetIterator = myArraySet[Symbol.iterator]();
//to show that the initial value of the @@iterator is the same as the initial values
console.log(myArraySet.values()); //output: SetIterator {"mango", "avocado", "apple"}
console.log(myArraySetIterator); //output: SetIterator {"mango", "avocado", "apple"}
console.log(...myArraySetIterator); //outputs: mango avocado apple
Summary
In this post, we have seen the strength and drawbacks of the different JavaScript loops. Not only that, but we have also tackled the iteration protocols, which basically has two types which are iterator and iterable protocol. Lastly, we have seen how built-in iterators work. Overall, this post has walked you through the puzzle of iterators.
I hope you have enjoyed this article, as I have enjoyed writing it. Stay tuned for more. Until next time, happy programming!
History
- 22nd August, 2020: Initial version