Generators are yet another addition to the JS standard introduced in the ES6 edition of the language. Generators are a new addition to JavaScript, although they have been existing in other programming languages such as Python, PHP, and C#.
What is a generator?
Generators are functions. Generators are special types of functions. Regular functions execute from start to finish, producing one value (or nothing). On the other hand, generators can produce/yield multiple values, per request, and they can suspend their execution between these requests. Generators are useful for creating more elegant asynchronous code and data streams.
But why do we need asynchronous code? You probably already know this, but JavaScript is single-threaded, and single-threaded execution model can block the entire UI is if the code is sluggish or unresponsive, or if the API takes a long time to complete. That makes the whole application slow and hell to interact with.
So how does a generator work?
A generator generates (duh!) a sequence of values, on a per request basis. The code will ask the generator specifically for a new value, and the generator function will either produce the new value or notify us that there are no more values to give back. The execution is suspended until a new value is requested and the generator continues where it stopped.
Let us see how this works by looking at a simple coding snippet:
function* CarGenerator() { yield "BMW"; yield "Mercedes"; yield "Toyota"; yield "Ford"; } for (let car of CarGenerator()) { console.log(car); }
Generator syntax
The first thing that ‘catches’ our eye is the asterisk next to the function
keyword. It creates a generator function and enables us to use the yield
keyword inside the body of the function. By decorating the function up with an asterisk, and yielding values inside the body we actually did everything we need in order to consume the generator. In the case above we iterated through the generator (like we would in an array!) and we got our yielded values back.
Let us rewrite the snippet above in order to make it a bit more interesting.
function* CarGenerator() { yield "BMW"; yield "Mercedes"; yield "Toyota"; yield "Ford"; } let carIterator = CarGenerator(); console.log(carIterator.next()); /* { done: false, value: "BMW" } */ console.log(carIterator.next()); /* { done: false, value: "Mercedes" } */
Generators are iterable! Something interesting happened when we created an iterator with let carIterator = CarGenerator();
. With the iterator, we get the ability to control the execution of the generator. We exposed the next()
method which is used to request new values from the generator. But what value?
Well, as you can see in the snippet above, the values that are returners are values connected to the yield
keyword, and those are value
and done
. When we encounter a yield
keyword the generator returns a new object with the value of the yield and the information of whether we reached the end of the iterator. The generator will stop its execution and wait for the next next()
method, which will wake the generator up and continue where it left off – producing new values!
So what happens if we have no more elements to produce. Let us take a look.
function* CarGenerator() { yield "BMW"; yield "Mercedes"; yield "Toyota"; yield "Ford"; } let carIterator = CarGenerator(); console.log(carIterator.next()); console.log(carIterator.next()); console.log(carIterator.next()); console.log(carIterator.next()); console.log(carIterator.next()); /* { done: true, value: undefined } */
The moment there is no more code to produce, the generator will return the ‘ending’ object, setting undefined
for the value
, and true
for done
.
Iterating a generator
We can walk through a generator an array using any kind of loop. Remember, the generator is iterable. Every time we can request a new value using next()
, until the last object produces a false
value for done.
function* CarGenerator() { yield "BMW"; yield "Mercedes"; yield "Toyota"; yield "Ford"; } let carIterator = CarGenerator(); let next; while (!(next = carIterator.next()).done) { console.log(next); }
We again create an iterator object by calling the generator function, and then we create the next
variable in which individual products of the generator will be stored. The loop might seem a bit contrived but it is really simple. On each iteration we call the next()
method of our iterator, and that value is stored in the conveniently named next
variable. Additionally, we check whether we reached the last iterator object. If the done
property is false
, we keep on looping the generator.
Sending data to the generator
Above we have seen how we can produce values from a generator using the yield
keyword. But we can also achieve two-way communication by sending data to the generator. This is useful in cases in which we produce a certain value, use that value to generate a new value, and then send that newly generated value back to the generator. We do that by simply using arguments like we would in any other regular function.
function* CarGenerator(property, value) { let carObject = {}; carObject[property] = value; let newCar = yield carObject; yield newCar; } let carIterator = CarGenerator("color", "blue"); let carObject = carIterator.next(); console.log(carObject); let updatedCarObject = { color: "pink" } let carIteratorObject2 = carIterator.next(updatedCarObject); console.log(carIteratorObject2)
A cool thing to notice is that we sent values to the generator two times, initially by setting the color to blue and yielding the first value of carObject
. Then we set a new object with the color pink and yielded newCar
inside the generator method, which produces a completely different object! The generator function accepts initial arguments and any additional passing of arguments every time we call the next()
method. Then we wake up the generator and resume the execution using the passed in value.
Final Words
Although Promises might be more widely used async structures the generators are also pretty important. There are other more advanced generator concepts to learn. For example how to yield to another generator, by delegating the execution of one generator to the other. If you are interested in learning more please click on the link.
Otherwise for more articles please click below, or check the blog.
- Largest Palindrome Product
- Largest Prime Factor
- Even Fibonacci Numbers
- Multiples of 3 or 5
- How to find the missing number in a given integer array of 1 to 100?
- Understanding Javascript prototypes
- Difference between Promise.all() and Promise.race()
- JavaScript generators
- Using this and arguments
- Arrow functions in JavaScript