Function generators in for loops

Viewed 1278

I'm trying to have a better understanding of how generators work in javascript.

From MDN:

The function* declaration (function keyword followed by an asterisk) defines a generator function, which returns a Generator object.

function *range(from, to) {
    var counter = from;
    while(to >= counter) {
        yield counter
        counter++
    }        
}

for (var r of range(5, 10)) {
    console.log( r );
}

// print: 5, 6, 7, 8, 9, 10

I'm not sure how I understand exactly what's happening in the snippet above.

Aren't generators supposed to be called, stored as (generator) objects and then called by the next() method. (like below)

function *foo () {
      yield 'woo';
 }
 var G = foo();
 console.log( G.next() );

In the code above, on line 4 with var G = foo(); I am not calling a function and creating new execution context, this should just return a generator object (and store it under the label G).

I'll invoke the actual function foo, when I call the next() method on it, on line 5. At that point, I'm creating an execution context, executing the code inside foo and yielding out the string "woo".

How the first snippet is exactly supposed to work?

2 Answers

Calling a generator function will return an iterator (the object with a .next function), and a for..of loop will automatically iterate over iterable objects. While you can store the iterator in a variable beforehand:

const iter = range(5, 10);
for (var r of iter) {
  ...
}

it's not necessary - the for..of only needs a single reference to an iterator, after all.

You could imitate this in code by passing a single reference to an iterator into a function that calls each .next function until the iterator is exhausted:

function *range(from, to) {
    var counter = from;
    while(to >= counter) {
        yield counter
        counter++
    }        
}

iterate(range(5, 10), num => {
  console.log(num);
});

function iterate(iterator, callback) {
  while (true) {
    const { value, done } = iterator.next();
    if (done) return;
    callback(value);
  }
}

As you can see, there's no need to store the iterator in a variable before passing it to iterate, just like you can use the range(5, 10) call directly to a for..of loop, because the loop (or function)'s internals do all the iteration for you.

So, the short answer to your question is that the for ... of loop actually only expects an object which conforms to the Iterable Protocol. This is an object which has a function bound to the symbolic key Symbol.iterator that returns an iterable object.

Generators actually have this property and the next function (which is required for implementations of the iterator protocol). You can see this in the following snippet.

const f = function*() {
  let i = -1; 
  while(true){
        i = i + 1; 
        yield i;
  }
};

const generator = f();
console.log(generator[Symbol.iterator]);
console.log(generator.next);

So, this is why you don't need to create an instance of the generator object for the for...of loop nor call next explicitely. This can be handled automatically due to the contract of the Iterable protocol.

That being said, you can mimic the behavior of the for...of loop with a while loop by creating the iterator object from the generator object (or calling next directly) as follows:

const f = function*() {
  let i = -1; 
  while(true){
        i = i + 1; 
        yield i;
  }
};

const generator = f();
const iterator = generator[Symbol.iterator]();
    
let j = 0;
let next;
while(j < 5) {
      next = iterator.next();
      console.log('next: ', next);
      j = next.value;
}

By principle, this is probably also what the native code for the for...of loop does.

Related