Generators in Javascript

What are generators and how can I use them?

How do you write loops in Javascript - Simple for/while? Recursion? In other less exciting ways?

How do you throttle and control function execution? Debounce function? Build your own?

What if you could produce the right mix of reusable functions, looping and throttling in one go? Enter generators.

What are Generators?

Generators are functions that lets you can control iteration. You can use generators to suspend execution of a function while saving the context for continuing the execution at a later time.

MDN defines generators as functions which can be exited and later re-entered.

Let’s understand more with an example -

function* getNums() {
  let i = 0;

  yield i++;
  yield i++;
  yield i++;
  yield i++;
}

const nums = getNums();

console.log(nums.next().value);
console.log(nums.next().value);
console.log(nums.next().value);
console.log(nums.next().value);

// 0 1 2 3

In the code block -

  1. We have defined the generator function using function* prefix
  2. We used a keyword yield indicates to Javascript that the function needs to go into sleep and return the said value
  3. Initiated the function
  4. Called the generator function multiple times - with each time stopping at the next yield to return a value

Simple enough?

What’s mondo interesting about this function is the ability to store a state without impacting the program flow. And each time we invoke the function with a next(), the state is back with a bang and execution continues as if nothing happened.

Defining generators

We can use any of the following ways to define generator functions-

function* func1() {}

function * func2() {}

function *func3() {}

let func4 = function*() {};
let func5 = function* () {};
let func6 = function *() {};

I just stick to what Prettier tells me to do.

Also, we cannot use arrow functions for generators yet.

Initializing generators

Once defined you don’t use generator functions like normal functions, instead you should initiate the generator.

const nums = getNums();

This returns an iterator, which you use like so -

console.log(nums.next().value);

The above statement returns an object at the yield statement within generator. So yield is like a return which is not a return :)

console.log(nums.next());
// { value: 0, done: false }
console.log(nums.next().value);
// 1

The done prop within the object indicates whether the generator has anything pending to do after the current yield. Consider the below example -

function* getNums() {
  let i = 0;

  yield i++;
}

const nums = getNums();

console.log(nums.next());
// { value: 0, done: false }
console.log(nums.next());
// { value: undefined, done: true }
console.log(nums.next());
// { value: undefined, done: true }

yield always returns the same object with same props.

You could technically do a return within a generator function, but any yield (or any other statement) will not execute after return.

function* getNums() {
  let i = 0;
  yield i++;
  return; // all done with second next() call
  yield i++;
}

const nums = getNums();

console.log(nums.next());
// { value: 0, done: false }
console.log(nums.next());
// { value: undefined, done: true }
Loops in a generator

No one ever said, “let’s make life complicated”. But here we are.

yield within a loop functions exactly as you would expect it to-

  • function execution starts
  • control goes into loop
  • encounters yield
  • returns the object and goes into coma
  • with the next next() control revives itself, finds itself in a loop, returns object and so on

    function* getNums() {
    for (let i = 0; i < 3; i++) {
    yield i;
    }
    }
    
    const nums = getNums();
    
    console.log(nums.next());
    console.log(nums.next());
    console.log(nums.next());
    console.log(nums.next());
    
    /* output
    { value: 0, done: false }
    { value: 1, done: false }
    { value: 2, done: false }
    { value: undefined, done: true }
    */
    
Generator chains

yield can call another generator. (And that’s really perfect since you can chain them to mess up one’s mind.)

function* getNums() {
  let i = 0;
  yield* getNumsNums();

  yield i++;
}

function* getNumsNums() {
  yield "what is life?";
  yield "what is I?";
}

const nums = getNums();

console.log(nums.next());
console.log(nums.next());
console.log(nums.next());
console.log(nums.next());

/* output
{ value: 'what is life?', done: false }
{ value: 'what is I?', done: false }
{ value: 0, done: false }
{ value: undefined, done: true }
*/

Where could generators be useful?

All we described about generators is well and dandy, but there’s no hope for them until we find a use case (or two).

Here’s what smarter people are using generators for -

  1. Generate an (almost) infinite loop that increments once every call. This is useful for use cases like sliders that keep going round and round
  2. Use generators in place of recursion. We will get back to our favourite algorithms to generate Fibonacci series and such in a bit
  3. Throttle function calls. For example, call external API for one request at a time (and that’s a totally practical scenario. Stop smirking.)
  4. Use promises within generators to have async functions with throttling! See the previous point
  5. Store states during different instances of the program life cycle, suspend and carry on with the task later. For example, generating unique ids in our algorithm
comments powered by Disqus