Basic Generators in JavaScript

I have been watching a movie last night when my mind spun on a different thread and remembered a JavaScript language feature that have existed for some time now, but I have never had the chance to use. At least, directly.

We do bleeding edge JavaScript at the office. That means we have all these new language features at our disposal as early as possible through the use of Babel. We write JavaScript code using the newest language specification (ECMAScript 6/7) and our code gets transpiled into ECMAScript 5. We have been using all the nifty features such as importasync/awaitspread/rest operators and destructuring as early as last year. These are just the new ES6 features that I can think of off the top, maybe because they are the most practical ones.

There is one feature, however, that can be really powerful but I have not really been able to leverage. They are generators. Prior to V8 v5.5 and Node v7.6.0, Babel's async/await and other asynchronous libraries around has been using generatorsunder the hood to implement this feature.

But what are generators? According to the venerable MDN page:

A generator is a special type of function that works as a factory for iterators. A function becomes a generator if it contains one or more yield expressions and if it uses the function* syntax.

MDN's definition is clear and straightforward, but let me rephrase it from what I have understood. Aside from producing iterables, think of a generator as a function that you can play and pause. This characteristic enables it to implement asynchronous programming, and when used with promises, you can come up with all sorts of things- including your own async library if you want to make one for learning purposes.

Let's dig into some basic code examples to solidify our understanding of generators:

function* counter() {
  for (let i = 0; i < 5; i++) {
    yield i
  }
}

This function was declared using function* and has a yield inside the function body, so this must be a generator. When we invoke it and assign the result to a variable like so, let c = counter(), we get back an iterable object that we can use to iterate over the values of i. An iterable object in JavaScript must have a next()method. This method returns an object that contains a value and a done property. Let's see that in action:

/***************************************************
  Using next() to step through the values explicitly
****************************************************/
let c1 = counter();

console.log(c1.next().value);
// 1
console.log(c1.next().value);
// 2
console.log(c1.next().value);
// 3
console.log(c1.next().value);
// 4
console.log(c1.next().value);
// 5

/***************************************************
  Using a for-of loop
****************************************************/
let c2 = counter();

for (const num of c) {
  console.log(c);
}

// 1
// 2
// 3
// 4
// 5

/***************************************************
  Using the done property explicitly
****************************************************/
let c3 = counter();

let i = c3.next();

while (!i.done) {
  console.log(i.value);
  i = c3.next();
}

// 1
// 2
// 3
// 4
// 5

We went through three different ways on how to iterate over the iterable that was returned by our counter generator. On the first example, we manually stepped through the iterator by using next(). We know that next() returns an object with a valueand a done property, and so we were able to chain .value every time we log the iteration to the console. This shows us one of the concepts that we have discussed earlier: we were able to play and pause the generator's execution by using the next() method. Another interesting thing is that it remembers its internal statethrough its iterations.

It works this way: the generator function stops immediately at every yield statement, and passes the value on its right to the object being returned by next(). We used a loop on our example, and by doing so, the loop gets suspended every time it encounters a yield statement.

Another thing worth knowing is that we can alter the generator's internal state from outside the generator by passing in an argument to next():

function* counter (limit) {
  for (let i = 1; i <= limit; i++) {
    let j = yield i;
    if (j) limit = j;
  }
}

/***************************************************
  Passing a value to next() to alter internal state
****************************************************/
const c1 = counter(2)

console.log(c1.next().value); // 1
console.log(c1.next().value); // 2
console.log(c1.next().value); // undefined

/***************************************************
  Passing a value to next() to alter internal state
****************************************************/
const a2 = counter(2)

console.log(c2.next().value); // 1
console.log(c2.next().value); // 2
console.log(c2.next(5).value); // 3
console.log(c2.next().value); // 4
console.log(c2.next().value); // 5

The example above is yet another contrived modification to our earlier example. This counter generator accepts an argument as the limit to the number of values it can generate. It has the same loop as the above example, except that the control is now dependent on the limit parameter that was passed to it.

Inside the loop body, we have declared a variable j that is being assigned to the value of yield. This expression is being followed by another control structure: an if statement that checks the value of j. The value of j will replace the value of limit if it has a truthy value.

As I have mentioned prior to showing the examples, we can control the internal state of generators by passing an argument to the next() method. This argument will become the value of yield inside the generator, and as such we can assign it to control its behavior.

This can be seen above where we both declared a generator with an initial limit of 2 values. On the first one, we did not pass an argument to next() and so we were only able to iterate through two values. On the second example, we did the same thing, but we passed in a value of 5 as an argument to next(). This altered the generator's internal limit from two to five values, enabling us to get three more values out of it.



On this post, we have learned about the basics of ES6's generators. We went through the basic implementation and usage through some simple examples. We found out that generator functions are declared using the function* keyword, and contains at least one yield statement/expression. We also found out that a generator produces and iterable with a next() method. Since this post is getting long, I have decided to split this post into two. On my next post, we will explore how to implement basic async/await functionality through the use of generators and promises.