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
- 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 }
Using return in your generator function does return an object containing the value but also setting the done property to true. So, returning using the return keyword instead of the yield keyword is an effective way to end the generator function.
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 }