https://codeburst.io/what-are-javascript-generators-and-how-to-use-them-c6f2713fd12e
Generators are functions that you can use to control the iterator. They can be suspended and later resumed at any time.
Declaring generators
1 2 3 4 5 |
function * generator () {} let generator = function * () { } |
However, we cannot create a generator using the arrow function.
We can create generators like this:
1 2 3 4 5 6 7 |
class MyClass { *generator() {} } const obj = { *generator() {} } |
code version
1 2 3 4 |
for (let i = 0; i < 5; i += 1) { console.log(i); } // this will return immediately 0 -> 1 -> 2 -> 3 -> 4 |
generator version
But the most significant change is that it does not ring immediately. And this is the most important feature in generators — we can get the next value in only when we really need it, not all the values at once.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function * generatorForLoop(num) { for (let i = 0; i < num; i += 1) { yield console.log(i); } } const genForLoop = generatorForLoop(5); genForLoop.next(); // first console.log - 0 genForLoop.next(); // 1 genForLoop.next(); // 2 genForLoop.next(); // 3 genForLoop.next(); // 4 |
Yield
It’s a bit like return, but not. Return simply returns the value after the function call, and it will not allow you to do anything else after the return statement.
1 2 3 4 5 6 7 8 9 10 11 12 |
function withReturn(a) { let b = 5; return a + b; // due to return from above: b = 6; // we will never re-assign b return a * b; // and will never return new value } withReturn(6); // 11 withReturn(6); // 11 |
with yield, we save the result of the executing line.
1 2 3 4 5 6 7 8 9 10 11 |
function * withYield(a) { let b = 5; yield a + b; // queue result list b = 6; yield a * b; // queue result to list. } const calcSix = withYield(6); calcSix.next().value; // 11 calcSix.next().value; // 36 |
Yield returns a value only once, and the next time you call the same function it will move on to the next yield statement.
Also in generators we always get the object as output. It always has two properties value and done. And as you can expect, value – returned value, and done shows us whether the generator has finished its job or not.
1 2 3 4 5 6 7 8 9 |
function * generator() { yield 5; } const gen = generator(); gen.next(); // {value: 5, done: false} gen.next(); // {value: undefined, done: true} gen.next(); // {value: undefined, done: true} - all other calls will produce the same result |
Naturally, it will obey the laws of execution. If we have a return, anything after it will never be executed.
1 2 3 4 5 6 7 8 9 10 11 |
function * generator() { yield 1; return 2; yield 3; // we will never reach this yield } const gen = generator(); gen.next(); // {value: 1, done: false} gen.next(); // {value: 2, done: true} gen.next(); // {value: undefined, done: true} |
Yield delegator
Yield with asterisk can delegate it’s work to another generator. This way you can chain as many generators as you want.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function * anotherGenerator(i) { yield i + 1; yield i + 2; yield i + 3; } function * generator(i) { yield* anotherGenerator(i); } var gen = generator(1); gen.next().value; // 2 gen.next().value; // 3 gen.next().value; // 4 |
A great example would be in recursive functions. In order to call yourself, you need to yield delegate it
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
function *backwards(i) { yield i; if (i <= 0) return; // base yield *backwards(i-1); // yield delegate to recursive function } let backwardsGen = backwards(10); console.log(backwardsGen.next()); //{ value: 10, done: false } console.log(backwardsGen.next()); //{ value: 9, done: false } console.log(backwardsGen.next()); //{ value: 8, done: false } console.log(backwardsGen.next()); // { value: 7, done: false } console.log(backwardsGen.next()); // { value: 6, done: false } console.log(backwardsGen.next()); // { value: 5, done: false } console.log(backwardsGen.next()); // { value: 4, done: false } console.log(backwardsGen.next()); // { value: 3, done: false } console.log(backwardsGen.next()); // { value: 2, done: false } console.log(backwardsGen.next()); // { value: 1, done: false } console.log(backwardsGen.next()); // { value: 0, done: false } console.log(backwardsGen.next()); // { value: undefined, done: true } |
return
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function * generator() { yield 1; yield 2; yield 3; } const genx = generator(); console.log(genx.next()); // { value: 1, done: false } console.log(genx.return()); // {value: undefined, done: true} console.log(genx.next()); // { value: undefined, done: true } console.log(genx.return('Heeyyaa')); // {value: "Heeyyaa", done: true} genx.next(); // {value: undefined, done: true} - all next() calls after return() will return the same output |
Try Catch
You can wrap generators in try catch functions.
Whenever crash/error happens inside of the generator, our outside try/catch will take care of it.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function *foo(x) { var y = x.toUpperCase(); // could be a TypeError error! yield y; // RICKY } var it = foo("Ricky"); try { console.log(it.next()); } catch (err) { console.log( err ); // TypeError (from `toUpperCase()` call) } |
You can also put yield in your generator function. When you have your generator variable, you can call throw on it, and it will throw the error right at where your yield is.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function *foo(x) { try { yield x; console.log( "x: " + x ); // may never get here! } catch (err) { console.log( "Error: " + err ); } } var it = foo("A"); console.log(it.next()); // custom throw an error it.throw("hehe"); |
If you throw(..) an error into a generator, but no try..catch catches it, the error will (just like normal) propagate right back out (and if not caught eventually end up as an unhandled rejection).
1 2 3 4 5 6 7 8 9 |
function *foo() { } var it = foo(); try { it.throw( "Oops!" ); } catch (err) { console.log( "Error: " + err ); // Error: Oops! } |
Generator calling other generators
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function *foo() { yield 3; yield 4; } function *bar() { yield 1; yield 2; yield *foo(); // `yield *` delegates iteration control to `foo()` yield 5; } for (var v of bar()) { console.log( v ); } // 1 2 3 4 5 var it = bar(); console.log(it.next()); // 1 |
Receive return value
Distinction between yield and yield*:
– with yield, the result is whatever is returned at the current expression,
– but with the yield* expression, it iterates through generator function’s yields.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
function *foo() { yield 2; yield 3; console.log('FOO') return "foo"; // return value back to `yield*` expression } function *bar() { yield 1; // yield * iterates through the yields in function *foo() // yield * foo(); yield yield * foo() // yield * iterate through foo(), then the yield to the left of yield * will yield the return value console.log('4') yield 4; } var it = bar(); console.log(it.next()); // { value:1, done:false } console.log(it.next()); // { value:2, done:false } console.log(it.next()); // { value:3, done:false } console.log(it.next()); // { value: 'foo', done: false } console.log(it.next()); // { value:4, done:false } |
The Pause
A characteristic of using next is that the generator runs code up to the point of the yield. You will get the value where yield is. Then, the code pauses there.
In the below example, it shows this.
We first create a generator “foo”.
Then we create another generator “bar”.
1) We call next on the generator variable “it”, and the generator starts executing the code.
It hits the first yield, which gives 1.
The code pauses here.
2) Then our generator variable call the next next function.
The code continues from the first yield and continues executing.
It moves into foo and then hits the 2nd yield, where it gives 2 as value.
the code pauses.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
function *foo() { try { yield 2; // 2nd next } catch (err) { // 5 // catches error thrown. // There is no "yield" to pause // so we continue on with execution. } // 6 console.log("paaaaaause"); yield; // pause // 8 now, throw another error throw "Oops!"; } function *bar() { yield 1; // 1st next try { yield *foo(); } catch (err) { // 9 // catches error from 8 console.log( "bar caught: " + err ); } } // each next works like this. It says to run the code until it hits a yield // then it pauses. var it = bar(); // here, we hit the 1st next(). We run the code until we hit the first yield. // we get 1. Then we pause. // 1 console.log(it.next()); // { value:1, done:false } // we try to access the next "next()". So we move foward in code. // then we hit "yield 2". We pause here // 2 console.log(it.next()); // { value:2, done:false } // 3 console.log("it.throw - throw some error"); // 4 // we then throw error. We throw from the execution line, which is yield 2. it.throw( "Uh oh!" ); // will be caught inside `foo()` // foo caught: Uh oh! // 7 we then want to access the next yield. so we continue on with the code from where the // last yield was. Which is an empty yield. // 10 no more yield after the the error caught in 9. So we have nothing to show here. it.next(); // { value:undefined, done:true } --> No error here! |
3) We log we’re about to throw error.
4) We get the generator variable “it” and call “throw”.
At this point, we continue from the last yield, which is at line “yield 2” in the foo function. That is where the generator code has paused.
5) We catch the error in foo, and log it.
There is no yield so we DO NOT pause. Hence we continue execution.
6) We log at 6. Then we see a yield. Hence we pause, and return the execution to the generator variable.
7) We continue with execution on the generator variable “it”. We then call the next function again on the generator variable “it”.
It continues execution from where we left off in the generator, which was an empty yield.
It continues execution and we get to a throw “Oops”. The throw propogate to bar’s catch and gets caught.
It logs the bar catch, and bar finishes running.
Algorithm examples
Shellsort’s skip mechanism
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
let gaps = [2,5]; let array = [1,2,3,4,5,6,7,8,9,10]; function * skipper() { for (let gapIndex = 0; gapIndex < gaps.length; gapIndex++) { console.log(`Now jumping ${gaps[gapIndex]} across the array`); let skip = gaps[gapIndex]; for (let i = 0; i < array.length; i = i+skip) { yield array[i]; } } } let s = skipper(); let temp = s.next(); while (!temp.done) { temp = s.next(); if (temp.value) console.log(`${temp.value}`); } |
output:
Now jumping 2 across the array
3
5
7
9
Now jumping 5 across the array
1
6
Divide and Conquer concept
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
let array = [8, 99, 68, 77]; function * divideAndConquer(array, start, end) { console.log(`divideAndConquer - start: ${start}, end: ${end}`) // if (low == high) print // we have reached one single element case if (start === end) { console.log(`reached end`); yield array[start]; console.log("continuing...!"); return; } if (start > end) { console.log(`left recursion NOT AVAILABLE X`) return; } // get mid let mid = Math.floor((end - start)/2); console.log(`mid is ${mid}`); console.log(`LEFT recursion: [0, ${mid-1}]`) yield * divideAndConquer(array, 0, mid-1); console.log(`mid needs to be printed.`); yield array[mid]; console.log(`RIGHT recursion: [${mid+1}, ${end}]`) yield * divideAndConquer(array, mid+1, end); } let a = divideAndConquer(array, 0, array.length-1); console.log(`---1st next---`) console.log(`√ ${a.next().value}`); console.log(`---2nd next---`); console.log(`√ ${a.next().value}`); console.log(`---3rd next---`); console.log(`√ ${a.next().value}`); console.log(`---4th next---`); console.log(`√ ${a.next().value}`); |
output:
—1st next—
divideAndConquer – start: 0, end: 3
mid is 1
LEFT recursion: [0, 0]
divideAndConquer – start: 0, end: 0
reached end
√ 8
—2nd next—
continuing…!
mid needs to be printed.
√ 99
—3rd next—
RIGHT recursion: [2, 3]
divideAndConquer – start: 2, end: 3
mid is 0
LEFT recursion: [0, -1]
divideAndConquer – start: 0, end: -1
left recursion NOT AVAILABLE X
mid needs to be printed.
√ 8
—4th next—
RIGHT recursion: [1, 3]
divideAndConquer – start: 1, end: 3
mid is 1
LEFT recursion: [0, 0]
divideAndConquer – start: 0, end: 0
reached end
√ 8