For an object to become an iterator it needs to know how to access values in a collection and to keep track of its position in the list. This is achieved by an object implementing a next method and returning the next value in the sequence. This method should return an object containing two properties: value and done. It must have the [Symbol.iterator] as well, as this is key to using the for..of loop.

Street art of cartoon fish Photo by Sunyu on Unsplash

Build your first iterator

function tenMultiplesOfThree(){
  let start = 1;
  const multiple = 3;
  return {
    next: function() {
      if (start <= 10) {
        const product = multiple * start;
        start++;       
        return {
          value: product,
          done: false
        };
      }
      
      return {
        done: true
      };
    }
  };
}

let iterator = {
  [Symbol.iterator] : tenMultiplesOfThree
}

Below is a for..of using the above iterator. Without the [Symbol.iterator] the for loop will not work.

for(let number of iterator){
  console.log(number);
}
// -> 3, 6, 9, 12, 15, 18, 21, 24, 27, 30

Built-in iterators

String, Array, TypedArray, Map and Set implement the Symbol.iterator method on their prototype. It is also possible to iterate a collection of DOM elements like a NodeList.

for(let letter of "abc") {
  console.log(letter);
}
// -> "a" "b" "c"

Generators

You might be looking at the first code example and thinking it is pretty verbose with some boiler code. ES6 has some syntax sugar to help make this all look clean and succinct.

An example below of a generator:

function* firstTenMultiples(multiple = 3) {
  let start = 1;
  while(start <= 10){
    yield multiple * start;
    start++;
  }
}

console.log(firstTenMultiples().next());

let iteratorGen = {
  [Symbol.iterator]: firstTenMultiples
}

for(let number of iteratorGen) {
  console.log(number);
}
// -> 3, 6, 9, 12, 15, 18, 21, 24, 27, 30

The key features to defining a generator are function*. Essentially, yield pauses the execution of the function body when it is reached until the next call is made. In the above example you can see by calling the firstTenMultiples function we have access to next method which returns value and iterator state done. This is the same as defining our own iterator, except it’s more concise and easy to read.

I feel the asterisk makes this a bit awkward, in terms of remembering to use it and its position on function. I’m sure this is new syntax we will get used to.

function* firstTenMultiples(multiple = 3) {
  let start = 1;
  while(start <= 10){
    yield multiple * start;
    start++;
  }
}

let multiplesOfFour = firstTenMultiples(4);

for(let number of multiplesOfFour) {
  console.log(number);
}
// -> 4, 8, 12, 16, 20, 24, 28, 32, 36, 40

From the above you can see I did not need to assign the generator function to [Symbol.iterator] for for..of loop to work. Inspecting the called result of firstTenMultiples(4) you can see on the __proto__ it does have [Symbol.iterator] implemented. When calling a generator function it returns a generator object which is iterable.

Generator finished

function* returnExample(){
  yield "hello";
  return "world";
  yield "not reached"
}
const gen = returnExample();
console.log(gen.next()) // => { value: "hello", done: false }
console.log(gen.next()) // => { value: "world", done: true }
console.log(gen.next()) // => { value: undefined, done: true }

In the above example return ends the generator and sets the done state to true. Anything after that is not reached.

Delegate to another generator

function* countToThree(){
  yield 1;
  yield 2;
  yield 3;
}

function* firstTenMultiples(multiple = 3) {
  let start = 1;
  yield* countToThree();
  while(start <= 10){
    yield multiple * start;
    start++;
  }
}

let multiplesOfFour = firstTenMultiples(4);

for(let number of multiplesOfFour) {
  console.log(number);
}
// -> 1, 2, 3, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40

In the above example firstTenMultiples uses yield* to delegate to countToThree generator function. This results in listing one to three first, then multiples of four.

Pass in a value via next() method

function* firstTenMultiples(multiple = 3) {
  let start = 1;
  while(start <= 10){
    let reset = yield multiple * start;
    start++;
    if(reset) start = 1;
  }
}

let multiplesOfFour = firstTenMultiples(4);

console.log(multiplesOfFour.next().value); // 4
console.log(multiplesOfFour.next().value); // 8
console.log(multiplesOfFour.next().value); // 12
console.log(multiplesOfFour.next(true).value); // 4 (reset to start from beginning)
console.log(multiplesOfFour.next().value); // 8
console.log(multiplesOfFour.next().value); // 12

In the above example, the first parameter is used to reset the generator to start from the beginning. The last yield expression which paused the generator will use the parameter value as the result. Assigning yield to the variable reset, the value is undefined until a parameter is passed in, causing reset value to become true in this case.

Destructing values from iterator

const [a, b, c] = firstTenMultiples(5);
// -> a = 5, b = 10, c = 15

Spread values from iterator

const multiplesOfSix = [...firstTenMultiples(6)];
// -> [6, 12, 18, 24, 30, 36, 42, 48, 54, 60]

Hopefully, you find all these examples useful to get started with iterators and generators.

Next to read is my post on async iterators and generators.

References: