Deep dive into JavaScript's Iterators, Iterables and Generators

Deep dive into JavaScript's Iterators, Iterables and Generators

The default operations like for...of loop in JavaScript is powered behind the scene by the concepts of Iterators, Iterables, and Generators. These concepts serve as the protocol for iterating over data structures provided by JavaScript, such as the for...of loop, used to traverse the elements of specific data structures one by one. Throughout this article, we'll discuss and explore these concepts and learn how to implement them within your next JavaScript project.

Symbols for iterators

But before we explain these concepts, we'll take a short discovery on the JavaScript built-in method, Symbol.iterator, which serves as the unit block on implementing custom iteration in JavaScript.

// syntax
Symbol.iterator(); // Symbol(Symbol.iterator)

The concept of an Iterable is any structure that has the Symbol.iterator() key; the corresponding method has the following behavior:

  • When the for..of loop begins, it first looks for errors. If none are found, it then accesses the method and the object that the method is defined on.
  • The object will then be iterated in a for..of loop manner.
  • Then, it uses the next() method of that output object to get the next value to be returned.
  • The values given back will have the format done:boolean, value: any. The loop is finished when done:true is returned.
let list = {
  start: 2,
  end: 7,
};

list[Symbol.iterator] = function () {
  return {
    now: this.start,
    end: this.end,
    next() {
      if (this.now <= this.end) {
        return { done: false, value: this.now++ };
      } else {
        return { done: true };
      }
    },
  };
};

for (let i of list) {
  console.log(i);
}

//output
2
3
4
5
6
7

In JavaScript, the Array object is the most iterative iterable in nature. Still, any object does possess the nature of a group of the elements-expressing the nature of a range like an integer between 0 and Infinity, can be turned into an iterable. And the scope of this article would be to help you understand and implement custom iterables.

Iterator and Iterable in JavaScript

Iteration on any array of data involves the use of the traditional for loop to iterate over its elements as below:

let items = [1, 2, 3];

for (let i = 0; i < items.length; i++) {
    console.log(items[i]);
}

//output
'1'
'2'
'3'

The above code construct works fine but poses complexity when you nest loops inside loops. This complexity will result from trying to keep track of the multiple variables involved in the loops. To avoid the mistakes brought on by remembering loop indexes and reducing the loop's complexity, ECMAScript 2015 developed the for...of loop construct.

This gives birth to cleaner code, but most importantly, the for...of loop provides the capacity to iterate/loop over not just an array but any iterable object.

An iterator is any object which implements the next() method that returns an object with two possible key-value pairs:

{ value: any, done: boolean }

Using the earlier discussion on the Symbol.iterator(), we know how to implement the next() method that accepts no argument and returns an object which conforms to the above key-value pairs.

With this in mind, we implement a custom iteration process for a custom object type and successfully use the for…of loop to carry out iteration on the type. The following code block will attempt to create a LeapYear object that returns a list of leap years in the range of ( start, end) with an interval between subsequent leap years.

class LeapYear {
  constructor(start = 2020, end = 2040, interval = 4) {
    this.start = start;
    this.end = end;
    this.interval = interval;
  }

  [Symbol.iterator]() {
    let nextLeapYear = this.start;
    return {
      next: () => {
        if (nextLeapYear <= this.end) {
          let result = { value: nextLeapYear, done: false };
          nextLeapYear += this.interval;
          return result;
        }
        return { value: undefined, done: true };
      },
    };
  }
}

In the above code, we implemented the Symbol.iterator() method for the custom type LeapYear. We have the starting and ending points of our iteration in the this.startand this.end fields, respectively. Using the this.interval, we keep track of the interval between the first element and the next element of our iteration. Now, we can call the for…of loop on our custom type and see its behavior and output values like a default array type.

let leapYears = new LeapYear();

for (const leapYear of leapYears) {
    console.log(leapYear);
}

// output
2020
2024
2028
2032
2036
2040

As stated earlier, Iterable's are JavaScript structures with the Symbol.iterator() method, like Array, String, Set. And as a result, our above LeapYear type is an Iterabletoo. This is a basic idea ofIterablesandIterators`; for more reading, refer here.

Open Source Session Replay

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.

openreplay.png

Start enjoying your debugging experience -  start using OpenReplay for free .

Generators in Javascript

A generator is a function that can produce many output values, and may be paused and resumed. In JavaScript, a generator is like a traditional function that produces an iterable Generator object. This behavior is opposed to traditional JavaScript functions, which execute fully and return a single value with a return statement. The Generator uses the new keyword,yield, to pause and return a value from a generator function. For more reading, refer here. Below is the basic syntax for the declaration of a Generator:

function* generator() {} // Object [Generator] {}

Let’s explore how generators and yield work.

function* generate() {
  console.log("invoked first time");
  yield "first";
  console.log("invoked second time");
  yield "second";
}

let gen = generate();
let next_value = gen.next();
console.log(next_value);

// output
// <- invoked first time
// <- {value: 'first', done: false}

next_value = gen.next();
console.log(next_value);

// output
// <- invoked second time
// <- {value: 'second', done: false}

The Generator function yields an object similar to the Symbol.iterator two properties: done and value fields. In the context of our discussion, this provides that the Generator type is an Iterable. This means we can use the for…of loop on it to iterate through its returned values. The for…of loop enables us to return the value inside the value field without the need to return the done field as the for...of loop keeps track of the iteration. The for...of loop is a sugar syntax abstracting the implementation used in the previous code block.

function* generate() {
  console.log("invoked first time");
  yield "first";
  console.log("invoked second time");
  yield "second";
}

let gen = generate();

for (let value of gen) {
  console.log(value);
}

// output
//invoked first time
//first
//invoked second time
//second

Alternatively, we can also use the concept of Generator with our LeapYear type to implement iteration on it. See below code block for the updated code snippet:

class LeapYear {
    constructor(start = 2020, end = 2040, interval = 4) {
      this.start = start;
      this.end = end;
      this.interval = interval;
    }
    *[Symbol.iterator]() {
      for (let index = this.start; index <= this.end; index += this.interval) {
        yield index;
      }
    }
  }

The above refactored and succinct code will produce the same result as earlier:

// output
2020
2024
2028
2032
2036
2040

From the above output, using the Symbol.iterator method is much simpler than using the Generator concept. The above code snippets were used to implement a LeapYear iterator to generate leap years from the year 2020 to the year 2040.

Generator vs. Async-await - AsyncGenerator

It is possible to simulate the asynchronous behavior of code that "waits," a pending execution, or code that appears to be synchronous even if it is asynchronous, using both generator/yield concepts to imitate the async/await functions. A generator function's iterator (the next method) executes each yield-expression one at a time instead of async-await, which executes each awaitsequentially. For async/await functions, the return value is always a promise that will either resolve to a any value or throws an error. In contrast, the return value of the generator is always {value: X, done: Boolean}, and this could make one conclude that one of these functions is built off the other. Understanding that an async function may be broken down into a generator and a promise implementation are helpful. Tada!

Now, combining generator with async/await gives birth to a new type, AsyncGenerator. In contrast to a conventional generator, an async generator's next() method returns a Promise. You use the for await...of construct to iterate over an async generator. The below code snippet would attempt to asynchronously fetch external data when requested and yield the value.

let user = {
  request: 0,
  trials: 12,
};

async function* fetchProductButton() {
  if (user.trials == user.request) {
    return new Error("Exceeded trials");
  }
  let res = await fetch(`https://dummyjson.com/products/${user.request + 1}`);
  let data = await res.json();
  yield data;
  user.request++;
}

const btn = fetchProductButton();

(async () => {
  let product = await btn.next();
  console.log(product);
  // OR
  for await (let product of btn) {
    console.log(product);
  }
})();

The above code snippet will return the below JSON data:


//output
{
  value: {
    id: 1,
    title: 'iPhone 9',
    description: 'An apple mobile which is nothing like apple',
    price: 549,
    discountPercentage: 12.96,
    rating: 4.69,
    stock: 94,
    brand: 'Apple',
    category: 'smartphones',
    thumbnail: 'https://dummyjson.com/image/i/products/1/thumbnail.jpg',
    images: [
      'https://dummyjson.com/image/i/products/1/1.jpg',
      'https://dummyjson.com/image/i/products/1/2.jpg',
      'https://dummyjson.com/image/i/products/1/3.jpg',
      'https://dummyjson.com/image/i/products/1/4.jpg',
      'https://dummyjson.com/image/i/products/1/thumbnail.jpg'
    ]
  },
  done: false
}

The above code snippet would be a real-life example of simulating session access to always get a value as long as your session access isn't exhausted. Using the async generator provides flexibility as it could help make paginated requests to an external API or help decouple business logic from the progress reporting framework. Also, use a Mongoose cursor to iterate through all documents while updating the command line or a Websocket with your progress.

Built-in APIs accepting iterables

A wide variety of APIs supports iterables. Examples include the Map, WeakMap, Set, and WeakSet objects. Check out this MDN document on JavaScript APIs that accepts iterables.

Conclusion

In this article, you have learned about the JavaScript iterator and how to construct custom iteration logic using iteration protocols. Also, we learned that Generator is a function/object type that yields the kind of object value as an iterator. Although they are rarely utilized, they are a powerful, flexible feature of JavaScript, capable of dealing with infinite data streams, which can be used to build infinite scrolls on the front end of a web application, to work on sound wave data, and more. They can maintain state, offering an effective technique to create iterators. Generators may imitate the async/await capabilities when combined with Promises, which enables us to deal with asynchronous code in a more direct and readable way.

A TIP FROM THE EDITOR: For more on internal details of JavaScript, check out our JavaScript Types and Values, explained and Explaining JavaScript's Execution Context and Stack articles.

newsletter