A Generator is a function that can be paused and resumed at a later time, while having the ability to pass values to and from the function at each pause point. Generators are a special type of Iterator (see previous article).
Generators introduce new JavaScript syntax and keywords to use them. Generator functions are created using the function*
syntax.
let count = 0; function* myGen(start, end) { for(start; count < end; start++) { yield count++; } }
const genIterator = myGen(0,10); console.log(genIterator.next()); console.log(genIterator.next()); console.log(genIterator.next());
Did you notice, that invoking the Generator (myGen(0,10)
) does not execute its containing code? Instead it returns an Iterator. And as we know from a previous article, an Iterator provides a next()
method and can be looped over using a for..of loop.
When a value is consumed by calling the generator’s next
method, the Generator function executes until it encounters the yield
keyword. The rest of the Generator function code is not executed until you call next()
again. The function can be called as many times as desired, and returns a new Iterator each time. Each Generator may only be iterated once.
Object { done: false, value: 0 } Object { done: false, value: 1 } Object { done: false, value: 2 }
Sending values to and from the Generator via yield
Imagine your Generator function being like your tax office. The yield
keyword is like the only person working there (“I know, hard to image, right?”). You call that person (it.next()
) then he/she starts working until he/she found something to send you back (yield "Here you go"
), now he/she stops working until you call again. Meanwhile you can do with "Here you go"
whatever you see fit. Now you decide to call again, but this time you pass along some information (it.next("my tax report")
). The tax office person uses your info for internal processing until he/she finally sends you something back again ( { done: false, value: "all good" }
) . And this can continue until you call the office so often, that he/she eventually won’t have any new info for you and consider her work done ( { done: true, value: undefined }
). Even if you call the tax office knowing that there is nothing new, they show the decency to continue answering your calls.
Cool, now you understand how yield in a Generator function works and how you can send and retrieve info. But just to be sure, let your brain go through these steps again to understand it even better:
- You call
it.next()
- Now the generator code executes until it reaches
yield
. Code execution stops. - Yield always returns an object to the code “outside” where you called
next()
looking like this:{ done: boolean, value: any }
.
So if for example the Generator code saysyield "Hi"
, the returned object is{ done: false, value: "Hi" }
. - Outside, you can store “Hi” like this:
const genValue = it.next().value;
- Now let’s send a value to the Generator:
it.next("Whatever")
- The value “Whatever” is now inserted into the
yield
where it left off processing last time:const fromOutside = yield "Hi";
console.log(fromOutside); // "Whatever" - Code executes until the next
yield
which returns the object as mentioned above. And so on, until processing is done.
function* myGenerator() { const inp = yield "Hi"; console.log(inp); const name = yield; yield "My name is " + name; } const it = myGenerator(); console.log(it.next("A")); // { done: false, value: "Hi" } console.log(it.next("B")); // "B" { done: false, value: undefined } console.log(it.next("C")); // { done: false, value: "My name is C" } console.log(it.next("D")); // { done: true, value: undefined }
for..of over Generator
Remember, that a Generator is just an Iterator, and as such you can simply loop over it using for..of:
function* myGen() { let x = 0; while(x < 4) { yield x; x++; } } for(let val of myGen()) { console.log(val); } // 0, 1, 2, 3
Yield delegation
Yield delegation is when a Generator calls another Generator:
function* firstGen() { yield 1; yield 2; return 4; } function* secondGen() { const firstGenReturnVal = yield* firstGen(); yield 3; yield firstGenReturnVal; } const it = secondGen(); console.log(it.next()); // { done: false, value: 1 } console.log(it.next()); // { done: false, value: 2 } console.log(it.next()); // { done: false, value: 3 } console.log(it.next()); // { done: false, value: 4 }
Another example using only one Generator
function normalFunction() { return ["one", "two", "three"]; } function* gen() { yield* normalFunction(); } const it = gen(); console.log(it.next()); // { done: false, value: "one" } console.log(it.next()); // { done: false, value: "two" } console.log(it.next()); // { done: false, value: "three" }
Early Generator completion
Is there a way to let a Generator complete earlier than it actually would without changing the Generator code itself? Yes, using it.return()
or it.throw()
.
function* myGen() { let x = 0; while(x < 10) { yield x; x++; } } const it = myGen(); console.log(it.next()); { done: false, value: 0 } console.log(it.return()); { done: true, value: undefined } console.log(it.next()); { done: true, value: undefined }
Or you throw an Exception
function* myGen() { let x = 0; try { while(x < 10) { yield x; x++; } } catch (error) { console.error(error); } } const it = myGen(); console.log(it.next()); // { done: false, value: 0 } console.log(it.throw("Oh oh")); // "Oh oh" { done: true, value: undefined } console.log(it.next()); { done: true, value: undefined }