About generator functions in general
Recently, my friend Alex did a short demonstration on his great iterable library, which heavily relies on generator functions, so I decided to quickly write a short post on generators.
1. What is a generator function?
Generator is a function which can be exited and later re-entered. It keeps the state, and when it’s executed next time, it will pick up from the value where it was left off. They provide another layer of iteration and touch some deep, abstract principles of JavaScript.
Generators are marked with a little *
, and the syntax looks like this:
function* myGeneratorFunc() {
yield 'hello'
}
Generators return a generator object.
2. Iterables and iterators
A generator object has the features of both the iterable and iterator protocols.
2.1. Iterable protocol
An object is iterable if it has a method that is available through Symbol.iterator
. This will make the object iterable, i.e. it can be iterated over using, for example, for ... of
. This method will be called whenever the object needs to be iterated.
2.2. Iterator protocol
When this happens, an iterator will be returned, and the iterator is used to obtain the values through the next()
method.
next()
returns an object with two properties: value
and done
. If the generator has more values to generate (or yield
, more on that below), value
will contain the generated value, and done
will be false
. This basically means that there are more values left to return. When value
is undefined
, done
will become true
, and the generator has exhausted, and no more values left to return.
2.3. Examples
This sounds good, but it might not make much sense, so let’s take some examples.
This is a very basic iterable object:
const myIterable = {
[Symbol.iterator]: function*() {
yield 'hello'
yield 'world'
yield '!'
}
}
myIterable
has a property where the key is Symbol.iterator
, and it’s a method (a generator function), so it fulfils the requirements.
Iterable objects can be iterated, and the above method (with the Symbol.iterator
key) is getting called at iteration:
for (const word of myIterable) {
console.log(word)
}
// hello
// world
// !
The spread operator works the same way under the hood, it calls the method with the Symbol.iterator
key, too:
console.log(...myIterable) // 'hello' 'world' '!'
Some types, like strings are iterables by default:
const myString = 'hello'
for (const letter of myString) {
console.log(letter)
}
// h
// e
// l
// l
// o
Similarly, we can spread them, even into an array:
console.log([ ...myString ]) // [ 'h', 'e', 'l', 'l', 'o' ]
Arrays, of course, are iterables:
const arr = [ 1, 2, 3 ]
for (let num of arr) {
console.log(num)
}
// 1
// 2
// 3
and
console.log([ ...arr ]) // [ 1, 2, 3 ]
Sets, Map objects and typed arrays are also iterables.
But, numbers and objects are not:
const myNumber = 555
for (const digit of myNumber) {
console.log(digit)
}
// TypeError: myNumber is not iterable
3. The use case I like the most
Generators can be used in asynchronous programming as well as to generate values.
3.1. Infinite number generator
The good thing about generators is that we can create an infinite number generator without getting into an infinite loop. As it was stated above, the generator function returns an iterator object, and the next()
method can be called to get the value
and the done
properties. For the infinite number generator, value
will always increment by 1 (or whatever increment is set up), and done
will always be false
:
const infiniteGenerator = function*() {
let index = 1
while (true) {
yield index
index++
}
}
const gen = infiniteGenerator()
console.log(gen.next().value) // 1
console.log(gen.next().value) // 2
console.log(gen.next().value) // 3
console.log(gen.next().value) // 4
console.log(gen.next().value) // 5
console.log(gen.next().value) // 6
console.log(gen.next().value) // 7
console.log(gen.next().value) // 8
// and so on...
The spread operator cannot be used here, because there’s nothing to control how many numbers are extracted from the generator. Applying the spread operator will lead to an infinite loop.
3.2. Create ranges
Using the above principle, we can easily write a number range generator. The function below will generate the numbers between 1 and 5:
const range = function*(firstNumber, lastNumber) {
let index = firstNumber
while (index <= lastNumber) {
yield index
index++
}
}
const gen = range(1, 5)
console.log(gen.next()) // { value: 1, done: false }
console.log(gen.next()) // { value: 2, done: false }
console.log(gen.next()) // { value: 3, done: false }
console.log(gen.next()) // { value: 4, done: false }
console.log(gen.next()) // { value: 5, done: false }
console.log(gen.next()) // { value: undefined, done: true }
The numbers in the range can be accessed via gen.next().value
. After all numbers have been yielded, value
will become undefined
indicating that the generator has exhausted.
It’s safe to use the spread operator here, because the while
condition won’t always be true
, and this way we can easily create an array of numbers:
console.log([...range(1, 5)]) // [ 1, 2, 3, 4, 5 ]
We don’t have to invoke range
first and then call next
, the spread operator does it all for us.
4. Summary
Although generators are not very often used, they can be useful in some cases, and the way they work goes down deep how JavaScript works.
Generators yield values which can be accessed via the next()
method. Some types like strings, arrays or maps have generators built in and they are called iterables.
Thanks for reading, and see you next time.