Not long since I started to learn Python, I learnt about iterators and generators. I always heard about in JavaScript as well, in that similar concepts were introduced in ES6. But I never took a close look at it. However, as I spent more time on Python, I found it important to figure out the counterpart concepts in JavaScript (the interesting part when learn another language — it helps you get better in your old one).
So in this blog, we are going to dive deep into Iterators (and Generators in next blog) in JavaScript.
Processing each of the items in a collection is a very common operation. JavaScript provides a number of ways of iterating over a collection, from simple
for
loops tomap()
andfilter()
.Iterators and Generators bring the concept of iteration directly into the core language and provide a mechanism for customizing the behavior of
for...of
loops.
Iterable
Before jump into Iterators we also need to understand what is an Iterable — essentially, any objects that can be used in for..of
are called iterable. But what does for..of
take in? An object that implements the iterable protocal.
The iterable protocol allows JavaScript objects to define or customize their iteration behavior. Some built-in types are built-in iterables with a default iteration behavior, such as Array
or Map
, while other types (like an Object
below )are not. Still you get a gut feeling that the object below should be iterated in some certain way.
const iterObj = {
nestedObj: {
bread: [
'Baguette',
'Bagel',
'Toast'
],
milk: [
'Goat',
'Cow',
'Oat'
],
egg: [
'Scrumbled',
'Sunny side up',
'Boiled'
],
},
Often times, we end up writing a customised function to iterate each element in an customised object like above. But this is not easily maintainable. With iterable, however, we can generalise any “iterable” object and iterate through it easily. That’s what ECMA pushed out in ES6.
In order to be an iterable, an object must implement the @@iterator
method, meaning that the object must have a property with a @@iterator
key which is available via constant Symbol.iterator
. Reason that it uses Symbol data type is to ensure the the property name will be unique.
The definition of [Symbol.iterator]
uses Computed Property Names and returns a zero-argument function that returns an object, conforming to the iterator protocol.
So what is iterator protocol?
The iterator protocol defines a standard way to traverse a sequence of values.
An object is an iterator when it implements a next()
method and returns an object with at least two properties
- done: boolean
false
if the next value in the sequence is still available. The done
property is optional in this case.
true
if the iterator has reached the end of its sequence. In this case, value
optionally specifies the return value of the iterator.
- value: any
It represents any JavaScript value returned by the iterator. Can be omitted when done
is true
.
There are many built-in iterables in JavaScript:
Arrays, Strings, Maps, Sets and any thing that can put inside a for-of
loop. The popular destructuring operation and spread operator also requires an iterable as object.
To see an example using the iteration protocol, we can look at a random string.
let someString = 'hi';
console.log(typeof someString[Symbol.iterator]); // "function"
String
's default iterator returns the string's code points one by one:
let iterator = someString[Symbol.iterator]();
console.log(iterator + ''); // "[object String Iterator]"
console.log(iterator.next()); // { value: "h", done: false }
console.log(iterator.next()); // { value: "i", done: false }
console.log(iterator.next()); // { value: undefined, done: true }
You can redefine the iteration behavior by supplying our own @@iterator
:
// need to construct a String object explicitly to avoid auto-boxing
let someString = new String('hi');someString[Symbol.iterator] = function () {
return {
next: function () {
return this._first ? {
value: 'bye',
done: (this._first = false)
} : {
done: true
}
},
_first: true
};
};
Notice how redefining @@iterator
affects the behavior of built-in constructs that use the iteration protocol:
console.log([...someString]); // ["bye"]
console.log(someString + ''); // "hi"
That’s so much of it today. You may notice that it is quite a hassle to construct an iterator inside an iterable. We need to:
- implement
Symbol.iterator
function. - implement
next
function. - make sure the return object of
next()
has required propertiesvalue
anddone
. - save the state of the iterator to calculate when the last item gets retrieved so
done
is set tofalse
.
So in next blog we will look at how Generator
can save us from all the hassle.
Happy Reading!