Generators

Generators

Iterators on Demand

Generators are special functions in JavaScript, marked by the function* syntax, that can pause and resume their execution. This superpower is made possible by the yield keyword. Unlike regular functions that run to completion or terminate with a return, generators produce a sequence of values on demand.

function* numberGenerator() {
  let i = 0;
  while (true) {
    yield ++i;
  }
}
const iterator = numberGenerator();

console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3
// ... and so on

We can also pass a value to the location where the generator previously yielded. Thus enabling too and fro communication between the generator function and the code calling the function from elsewhere.

function* someRandomFunction(){
 const num = 1
 const newNum = yield num
 yield 1 + newNum
 yield 6
}
const returnNextElement = someRandomFunction()
const element1 = returnNextElement.next().value // 1
const element2 = returnNextElement.next(2).value // 3
const element3 = returnNextElement.next() // { value: 6, done: false }
returnNextElement.next() // { value: undefined, done: true }

When we pass an argument during a function call, the passed value will be placed where the latest executed yield statement was present.

How Generators Work (Under the Hood):
Generators leverage the closure property of the function and store the snapshot of the function state when a yield statement is encountered. Thus, for the consecutive calls it remembers the previous statement it has executed and resumes from that point.

function someRandomFunction(array){
 let i = 0
 const inner = {next :
     function(){
         const element = array[i]
         i++
         return element
      }
 }
 return inner
}
const returnNextElement = someRandomFunction([1,2,3])
const element1 = returnNextElement.next() // 1
const element2 = returnNextElement.next() // 2

This is not technically a generator function* since behaviour of yield is not implemented. But it beautifully mimics the behavior. It demonstrates how state (i) is managed to provide the next element in a sequence with each next() call.

Briefly -

Iterator: Decouples the task of iteration of data structure from the processing of an item.

yield: The heart of a generator function. yield acts as a strategic pause point. It sends a value back to the caller and remembers its state to resume from the same point on the next next() call which is very advantageous in specific scenarios (some are listed below).

Advantages and Use Cases:
Infinite Series: Creating sequences which adhere to iterator protocol.
Lazy Evaluation: Defer calculations until needed, improving efficiency when working with large datasets.
Readable Pipelines: Build data-processing steps in a chainable manner.
Cleaner Asynchronous Flows: Generators, especially when combined with promises, can simplify asynchronous code patterns. Checkout the article below where I have implemented async with function* and promise.