Table of contents
I learned about Iterators and Generators in JavaScript while solving a problem. So, we will be discussing these concepts here and trying to understand the concepts using the same problem that I encountered.
Before looking at the problem and understanding how to solve it, let's start with some theory...
Iterators
Iterables are objects which could be iterated using the for...of
loop.
Anything iterable is an object which will have the Symbol.iterator
method in it and when we try to iterate it or want it to behave like an iterable(using for...of
loop), the Symbol.iterator
method would be invoked which would return an object (iterator). The returned object would have the next
method in it, which would produce an object with the value and status of the iteration (using boolean) - whether any other value is present or not to be iterated.
A method(Symbol.iterator
) returning an object(iterator) which has another method(next
) in it ๐ค - complicated, right?
Let's understand the above statements better by solving the problem using iterators now.
The problem:
Q. Can you create a range(from, to) that makes the following work?
for (let num of range(1, 4)) {
console.log(num)
}
// 1
// 2
// 3
// 4
This is a simple one, could you think of more fancy approaches other than for-loop?
The simplest solution for the above problem would be as follows:
function range(from, to){
const arr = [];
for(let i = from; i <= to; i++){
arr.push(i);
}
return arr;
}
Look at the problem statement again, we are not required to return an array, but something iterable would be fine as we'll be using the
for...of
loop.
Now, we would be writing the range
function which would take 2 numbers as arguments and return an iterable.
// to make range() iterable
function range(from, to) {
return {
[Symbol.iterator]: function(){
// This returned object is known as the "iterator object" in the case of iterators and the "generator object" in the case of generators.
return {
from,
to,
next: function() {
if(this.from <= this.to){
return {value: this.from++, done: false}
}else{
return {done: true}
}
}
}
}
};
}
for (const item of range(1, 4)) {
console.log(item);
}
In the above code, whenever we try to iterate using the for...of
loop,
it calls the
Symbol.iterator
method once for the 1st time and gets the iterator object.Next time onwards
for...of
uses thenext
method to get the next value while iterating.
Don't confuse arrays with the iterables. Iterables are objects which implement the
Symbol.iterator
method in it. Arrays and strings have built-in implementation ofSymbol.iterator
along with many other methods and properties, not necessarily other iterables would have the same.
Generators
The generator function returns an object known as the "generator object" and has the next
method in it.
Example:
function *generatorExample(){
yield 1;
yield 2;
}
const generatorObject = generatorExample();
console.log(generatorObject.next()); // {value: 1, done: false};
console.log(generatorObject.next()); // {value: 2, done: false};
console.log(generatorObject.next()); // {value: undefined, done: true};
When the next
method is called, it executes the function till the 1st yield, then pause the execution and returns an object with 2 properties:
- value: yielded value.
- done: boolean, which says whether the function execution is finished or not.
The generator functions can be identified by the
*
in function declaration, right after thefunction
keyword(function*
) or before the function name(function*
name) without any space.
The same problem mentioned above could be solved using Generator functions quite easily as follows:
function range(from, to){
return {
[Symbol.iterator]: function* (){
for(let i = from; i <= to; i++){
yield i;
}
}
}
}
for (const item of range(1, 4)) {
console.log(item);
}
// 1
// 2
// 3
// 4
The range
function is still returning an object which has the Symbol.iterator
method in it. But the difference is the Symbol.iterator
is holding a generator function now. And as we know, the generator function returns the generator object with the next
method in itself - which we had to implement explicitly for iterators.
Generators are also iterable.
We can use the for...of
loop with generators then.
function *generatorExample(){
yield 1;
yield 2;
}
const generatorObject = generatorExample();
for(const item of generatorObject){
console.log(item);
}
// 1
// 2
Since a generator is also iterable, the solution can be simplified more as we don't have to implement the Symbol.iterator
method explicitly.
function *range(from, to){
for(let i = from; i <= to; i++){
yield i;
}
}
for (const item of range(1, 4)) {
console.log(item);
}
// 1
// 2
// 3
// 4
That's all for now ๐. Thanks for reading till now๐.
If you want to read more about these, refer to Iterators & generators - MDN.
Share this blog with your network if you found it useful and feel free to comment if you've any doubts about the topic.