ref – https://towardsdatascience.com/how-to-use-generator-and-yield-in-javascript-d1771bf698cd
A generator is a function that allows for the function to be exited and re-entered with its context (variable bindings) preserved.
Normal functions
1 2 3 4 5 6 |
function regularFunction() { console.log("I'm a regular function") console.log("Surprise surprice") console.log("This is the end") } regularFunction() |
—————–
Output
—————–
I’m a regular function
Surprise surprice
This is the end
1 2 3 4 5 6 7 8 9 10 |
function* generatorFunction() { yield "This is the first return" console.log("First log!") yield "This is the second return" console.log("Second log!") return "Done!" } |
What is function*?
That’s the syntax we use to declare a function as a generator.
What is yield?
The yield, will pause the function by saving all its states and will later continue from that point on successive calls.
Let’s call our generatorFunction and see what happens
1 |
generatorFunction() |
—————–
Output
—————–
generatorFunction {
__proto__: Generator
[[GeneratorLocation]]: VM272:1
[[GeneratorStatus]]: “suspended”
[[GeneratorFunction]]: ƒ* generatorFunction()
[[GeneratorReceiver]]: Window
[[Scopes]]: Scopes[3]
}
When we call a generator function, the function is not automatically triggered and instead,
it returns an iterator object.
What’s particular about this object is that when the method next() is called, the generator function’s body is executed until the first yield or return expression.
1 2 |
const myGenerator = generatorFunction() myGenerator.next() |
—————–
Output
—————–
{value: “This is the first return”, done: false}
generator run until the first yield statement and yielded an object containing a value property, and a done property.
1 |
{ value: ..., done: ... } |
The value property is equal to the value that we yielded
The done property is a boolean value, which is only set to true once the generator function returned a value. (not yielded)
Let’s invoke next() one more time and see what we get
1 |
myGenerator.next() |
—————–
Output
—————–
First log!
{value: “This is the second return”, done: false}
This time we first see the console.log in our generator body being executed and printing First log!, and the second yielded object. And we could continue doing this like:
1 |
myGenerator.next() |
—————–
Output
—————–
Second log!
{value: “Done!”, done: true}
Now the second console.log statement is executed and we now hit
1 |
return "Done" |
but this time the property done is set to true.
The value of the done property is not just a flag, it is a very important flag as we can only iterate a generator object once!.
try calling next() one more time:
1 |
myGenerator.next() |
—————–
Output
—————–
{value: undefined, done: true}
yield #
Yield over iterators
1 2 3 4 5 |
function* yieldArray(arr) { yield arr } const myArrayGenerator1 = yieldArray([1, 2, 3]) myArrayGenerator1.next() |
—————–
Output
—————–
{value: Array(3), done: false}
But that’s not quite what we wanted, we wanted to yield each element in the array, so we could try doing something like:
1 2 3 4 5 6 7 8 9 |
function* yieldArray(arr) { for (element of arr) { yield element } } const myArrayGenerator2 = yieldArray([1, 2, 3]) myArrayGenerator2.next() myArrayGenerator2.next() myArrayGenerator2.next() |
—————–
Output
—————–
{value: 1, done: false}
{value: 2, done: false}
{value: 3, done: false}
We can also use yield* to produce the same results.
1 2 3 4 5 6 7 8 |
function* yieldArray(arr) { yield* arr // * iterate over the array and yield each value } const myArrayGenerator3 = yieldArray([1, 2, 3]) myArrayGenerator3.next() myArrayGenerator3.next() myArrayGenerator3.next() |
—————–
Output
—————–
{value: 1, done: false}
{value: 2, done: false}
{value: 3, done: false}
by using yield* expression we can iterate over the operand and yield each value returned by it.
Yield * can be applied to other generators, arrays, strings, any iterable object.
Uses of Generators
The great thing about generators is the fact that they are lazy evaluated (meaning that the value that we get from invoking the next() method) is only computed after we specifically asked for it. This makes generators a good choice for solving multiple scenarios like the ones presented below:
Generating an infinite sequence
First we have a generator infiniteSequence, which is an iterator.
We calculate an infinite sequence in there and yield the number.
But because generators are lazy loaded, it won’t calculate the value until we ask for it by using .next(). Hence, our infinite sequence won’t run until stackoverflow. It will patiently wait for us to next().
1 2 3 4 5 6 7 |
function* infiniteSequence() { let num = 0 while (true) { yield num num += 1 } } |
We then loop over the iterator’s yield value and log it. We break if i is more than 10.
1 2 3 4 5 6 |
for(i of infiniteSequence()) { if (i >= 10) { break } console.log(i) } |
If we didn’t use generators, this program would crash because infiniteSequence would get stackoverflow or out of memory crash. But because its an iterator, we can lazy load and display its value like so:
—————–
Output
—————–
0
1
2
3
4
5
6
7
8
9
Implementing iterables
ref – http://chineseruleof8.com/code/index.php/2021/12/10/creating-an-iterator/
When you need to implement an iterator, you have to manually create an object with a next() method. Also, you have to manually save the state.
Imagine we want to make an iterable that simply returns I, am, iterable. Without using generators we would have to do something like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
const iterableObj = { [Symbol.iterator]() { let step = 0; return { next() { step++; if (step === 1) { return { value: 'I', done: false}; } else if (step === 2) { return { value: 'am', done: false}; } else if (step === 3) { return { value: 'iterable.', done: false}; } return { value: '', done: true }; } } }, } for (const val of iterableObj) { console.log(val); } |
—————–
Output
—————–
I
am
iterable.
With generators this is much simpler:
1 2 3 4 5 6 7 8 |
function* iterableObj() { yield 'I' yield 'am' yield 'iterable.' } for (const val of iterableObj()) { console.log(val); } |
—————–
Output
—————–
I
am
iterable.
notes
Generator objects are one-time access only. Once exhausted, you can’t iterate over it again. To do so, you will have to create a new generator object.
Generator objects do not allow random access as possible with for instance, arrays.
Since the values are generated one by one, you can’t get the value for a specific index, you will have to manually call all the next() functions until you get to the desire position, but then, you cannot access the previously generated elements.