Iterator Pattern
Structural: Iterator Pattern
What is the Iterator Design Pattern in JavaScript?
View Answer:
The objects participating in this pattern are:
Client -- In example code: the run() function
- references and invokes Iterator with a collection of objects
Iterator -- In example code: Iterator
- implements Iterator interface with methods first(), next(), etc
- keeps track of the current position when traversing the collection
Items -- In example code: Items
- individual objects of the collection getting traversed
In modern JavaScript, we often use the Iterator pattern without even knowing it. For example, when we use for...of
loops or the Array.prototype.forEach
method.
The built-in JavaScript data types like Array
, String
, Set
, Map
are all iterables because they implement the Iterable interface, meaning they expose a method whose key is Symbol.iterator
.
Here is a simple example of how you might use the Iterator Design Pattern in Modern JavaScript:
// Define the collection
let collection = {
items: ["Item1", "Item2", "Item3"],
[Symbol.iterator]() {
let index = -1;
return {
next: () => ({
value: this.items[++index],
done: index >= this.items.length
})
};
}
};
// Iterate over the collection using the for...of loop
for (let item of collection) {
console.log(item); // Output: Item1, Item2, Item3
}
In this example, the collection object implements the iterable interface by providing a method at the Symbol.iterator
key. This method returns an iterator object, which defines a next
method that returns the next value in the sequence.
The for...of
loop implicitly calls the collection's [Symbol.iterator]()
method, and then iterates over the returned iterator, logging each value to the console.
Detailed use without Symbol.Iterator:
let Iterator = function (items) {
this.index = 0;
this.items = items;
};
Iterator.prototype = {
first: function () {
this.reset();
return this.next();
},
next: function () {
return this.items[this.index++];
},
hasNext: function () {
return this.index <= this.items.length;
},
reset: function () {
this.index = 0;
},
each: function (callback) {
for (let item = this.first(); this.hasNext(); item = this.next()) {
callback(item);
}
},
};
function run() {
let items = ['one', 2, 'circle', true, 'Applepie'];
let iter = new Iterator(items);
// using for loop
for (let item = iter.first(); iter.hasNext(); item = iter.next()) {
console.log(item);
}
console.log('');
// using Iterator's each method
iter.each(function (item) {
console.log(item);
});
}
run();
/*
OUTPUT:
one
2
circle
true
Applepie
one
2
circle
true
Applepie
*/
const items = [1, 'hello', false, 99.8];
function Iterator(items) {
this.items = items;
this.index = 0; // to start from beginning position of array
}
Iterator.prototype = {
// returns true if a next element is available
hasNext: function () {
return this.index < this.items.length;
},
//returns next element
next: function () {
return this.items[this.index++];
},
};
//Instantiate object for Iterator
const iterator = new Iterator(items);
while (iterator.hasNext()) {
console.log(iterator.next());
}
/*
OUTPUT
1
hello
false
99.8
*/
Why use the Iterator Pattern in JavaScript?
View Answer:
The Iterator pattern belongs to which pattern category?
View Answer:
When should you utilize JavaScript's Iterator Pattern?
View Answer:
What are some of the benefits of using the Iterator pattern?
View Answer:
- Singular Responsibility Principle By separating cumbersome traversal algorithms into different classes, you may clean up the client code and collections.
- The Open/Closed Principle -- You can add new types of collections and iterators to existing code without affecting anything.
- Because each iterator object maintains its iteration state, you can concurrently iterate over the same collection.
- For the same reason, you can postpone an iteration and resume it later.
What are some of the Iterator pattern's drawbacks?
View Answer:
Drawbacks of the Iterator Pattern.
- Using the pattern may be overkill if your software works with simple collections.
- Using an iterator may be less productive than going over elements of specific specialized collections directly.
Are there any alternatives to using the Iterator pattern?
View Answer:
In which scenarios would you use the Iterator Pattern?
View Answer:
How does the Iterator Pattern differ from the Factory Pattern?
View Answer:
How does the Iterator Pattern promote Single Responsibility Principle?
View Answer:
How does the Iterator Pattern affect the time complexity of accessing collection elements?
View Answer:
Is the Iterator Pattern only suitable for homogeneous collections?
View Answer:
How does JavaScript's ES6 syntax enhance the Iterator Pattern?
View Answer:
What is the relationship between the Iterator Pattern and the Composite Pattern?
View Answer:
To illustrate how these patterns can be used together, let's consider a file system. In this system, a Directory
can contain Files
and other Directories
. Here's how you might model this using the Composite and Iterator patterns:
class File {
constructor(name) {
this.name = name;
}
[Symbol.iterator]() {
let done = false;
return {
next: () => {
if (!done) {
done = true;
return { value: this.name, done: false };
}
return { value: undefined, done: true };
}
};
}
}
class Directory {
constructor(name) {
this.name = name;
this.children = [];
}
add(child) {
this.children.push(child);
return this;
}
[Symbol.iterator]() {
let index = -1;
return {
next: () => {
if (++index < this.children.length) {
return { value: this.children[index], done: false };
}
return { value: undefined, done: true };
}
};
}
}
let root = new Directory('root');
let bin = new Directory('bin');
let usr = new Directory('usr');
let file1 = new File('file1');
let file2 = new File('file2');
root.add(bin).add(usr).add(file1);
bin.add(file2);
for (let file of root) {
if (file instanceof File) {
console.log(file.name); // file1, file2
} else {
console.log(`Directory: ${file.name}`); // Directory: bin, Directory: usr
}
}
In this example, both File
and Directory
are treated uniformly as FileSystemNode
. Both File
and Directory
define a [Symbol.iterator]()
method, which means they can both be iterated over using a for...of
loop. The difference is that a File
iteration always yields a single item, while a Directory
iteration can yield multiple items (other directories or files). This combination of the Composite and Iterator patterns allows us to work with complex tree-like structures in a very efficient way.