As it turns out, iterators can be written using generators. This can lead to some interesting use cases. Read on to understand the synergistic properties between these two JavaScript iteration concepts.
Mixing Generators Into Iterators
Let’s do a quick recap of generators (read our primer on generators here). Generator functions return generator objects when invoked. A generator object has a next
method, which returns the next element in the sequence. The next
method returns objects with a { value, done }
shape.
The following example shows an infinite fibonacci number generator. We then instantiate a generator object and read the first eight values in the sequence.
function* fibonacci() {
let previous = 0
let current = 1
while (true) {
yield current
const next = current + previous
previous = current
current = next
}
}
const g = fibonacci()
console.log(g.next()) // <- { value: 1, done: false }
console.log(g.next()) // <- { value: 1, done: false }
console.log(g.next()) // <- { value: 2, done: false }
console.log(g.next()) // <- { value: 3, done: false }
console.log(g.next()) // <- { value: 5, done: false }
console.log(g.next()) // <- { value: 8, done: false }
console.log(g.next()) // <- { value: 13, done: false }
console.log(g.next()) // <- { value: 21, done: false }
Iterators follow a similar pattern (you may read our primer on iterators here). They enforce a contract that dictates we should return an object with a next
method. That method should return sequence elements following a { value, done }
shape. The following example shows a fibonacci
iterable that’s a rough equivalent of the generator we were just looking at.
const fibonacci = {
[Symbol.iterator]() {
let previous = 0
let current = 1
return {
next() {
const value = current
const next = current + previous
previous = current
current = next
return { value, done: false }
}
}
}
}
const sequence = fibonacci[Symbol.iterator]()
console.log(sequence.next()) // <- { value: 1, done: false }
console.log(sequence.next()) // <- { value: 1, done: false }
console.log(sequence.next()) // <- { value: 2, done: false }
console.log(sequence.next()) // <- { value: 3, done: false }
console.log(sequence.next()) // <- { value: 5, done: false }
console.log(sequence.next()) // <- { value: 8, done: false }
console.log(sequence.next()) // <- { value: 13, done: false }
console.log(sequence.next()) // <- { value: 21, done: false }
Let’s reiterate. An iterable should return an object with a next
method: generator functions do just that. The next
method should return objects with a { value, done }
shape: generator functions do that too. What happens if we change the fibonacci
iterable to use a generator function for its Symbol.iterator
property? As it turns out, it just works.
The following example shows the iterable fibonacci
object using a generator function for its iterator. Note how that iterator has the exact same contents as the fibonacci
generator function we saw earlier. We can use yield
, yield*
, and all of the semantics found in generator functions hold.
const fibonacci = {
* [Symbol.iterator]() {
let previous = 0
let current = 1
while (true) {
yield current
const next = current + previous
previous = current
current = next
}
}
}
const g = fibonacci[Symbol.iterator]()
console.log(g.next()) // <- { value: 1, done: false }
console.log(g.next()) // <- { value: 1, done: false }
console.log(g.next()) // <- { value: 2, done: false }
console.log(g.next()) // <- { value: 3, done: false }
console.log(g.next()) // <- { value: 5, done: false }
console.log(g.next()) // <- { value: 8, done: false }
console.log(g.next()) // <- { value: 13, done: false }
console.log(g.next()) // <- { value: 21, done: false }
Meanwhile, the iterable protocol also holds up. To verify that you might use a construct like for..of
, instead of manually creating the generator object. The following example uses for..of
and introduces a circuit breaker to prevent an infinite loop from crashing the program.
for (const value of fibonacci) {
console.log(value)
if (value > 20) {
break
}
}
// <- 1
// <- 1
// <- 2
// <- 3
// <- 5
// <- 8
// <- 13
// <- 21
This was a fun trick. What would you use it for in a real-world program?