Mastering Iterators and Generators in JavaScript: A Beginner's Guide

Published on

Introduction

Generators and iterators are features introduced in ECMAScript 6 (ES6), which is also known as ES2015. These features provide a new way to iterate over collections of data and to generate values on the fly.

Generators allow you to define functions that can be paused and resumed, while iterators provide a standardized way to traverse collections of data. Let's dive deeper into these concepts.

Generators

A generator function is a special type of function that can be paused and resumed at any time, allowing you to generate a sequence of values on the fly.

To define a generator function, you use the function* syntax (the asterisk distinguishes it from a regular function).

Inside the generator function, you use the yield keyword to pause the function and return a value to the caller.

For example, let’s define a simple generator function that generates a sequence of numbers:

function* numberGenerator() {
  yield 1
  yield 2
  yield 3
}

In this example, when we call the numberGenerator function, it will return an iterator that we can use to iterate over the sequence of numbers. To do this, we use the next method of the iterator, like this:

const iterator = numberGenerator()

console.log(iterator.next().value) // 1
console.log(iterator.next().value) // 2
console.log(iterator.next().value) // 3

As you can see, each time we call the next method of the iterator, the generator function resumes execution and returns the next value in the sequence.

Iterators

An iterator is an object that provides a standard way to traverse a collection of data. In JavaScript, iterators are defined by the Iterable and Iterator protocols.

The Iterable protocol defines a method that returns an Iterator object, while the Iterator protocol defines a next method that returns an object with two properties: value and done.

To make an object iterable, you define a method on the object that returns an iterator. This method is called [Symbol.iterator], and it should return an object that conforms to the Iterator protocol.

For example, let’s define a simple iterable object that generates a sequence of numbers:

const numberIterable = {
  *[Symbol.iterator]() {
    yield 1
    yield 2
    yield 3
  },
}

In this example, we define a generator function inside the [Symbol.iterator] method to generate the sequence of numbers. We can now use a for...of loop to iterate over the sequence:

for (const number of numberIterable) {
  console.log(number)
}

// Output: 1, 2, 3

Real World Example

Suppose you are building a web application that allows users to upload and manage images. You want to implement a feature that automatically resizes the images to a certain size before saving them to your server.

To accomplish this, you can use generators and iterators to process the images in a stream-like fashion, allowing you to efficiently resize and save the images one by one.

First, you create a generator function that retrieves the uploaded images from the client-side and yields each image as a data URI

function* getImageDataURIs() {
  const imageFiles = document.querySelector('#image-input').files
  for (const file of imageFiles) {
    const reader = new FileReader()
    reader.readAsDataURL(file)
    yield new Promise((resolve) => {
      reader.onload = () => {
        resolve(reader.result)
      }
    })
  }
}

Next, you create an iterator function that retrieves each image data URI from the generator and resizes it using a library like sharp or whatever is your preferred library/API

async function* resizeImages() {
  const imageURIs = getImageDataURIs()
  for await (const uri of imageURIs) {
    const imageBuffer = Buffer.from(uri.split(',')[1], 'base64')
    const resizedImageBuffer = await sharp(imageBuffer).resize({ width: 500 }).toBuffer()
    yield resizedImageBuffer
  }
}

Finally, you create a function that saves each resized image to your server

async function saveResizedImages() {
  const resizedImages = resizeImages()
  for await (const imageBuffer of resizedImages) {
    await fetch('/api/images', {
      method: 'POST',
      body: imageBuffer,
      headers: {
        'Content-Type': 'image/jpeg',
      },
    })
  }
}

By using a generator and an iterator, you can easily process each uploaded image in a stream-like fashion, without having to load all the images into memory at once.

This can be especially useful if you are dealing with a large number of images, as it allows you to process them efficiently and without running into memory issues.

Conclusion

Generators and iterators are powerful tools that provide a new way to work with collections of data in JavaScript. Generators allow you to generate a sequence of values on the fly, while iterators provide a standardized way to traverse collections of data.

By using these features, you can write more efficient and scalable code in JavaScript.

Updates straight in your inbox!

A periodic update about my life, recent blog posts, TIL (Today I learned) related stuff, things I am building and more!

Share with others

Liked it?

Views

You may also like

  • javascript

    Integration Testing in JavaScript with Jest and Nock: A Beginner's Guide

    Integration testing is an essential part of the software development process, and it ensures that all the components of your application work together as expected. In this article, we'll explore the basics of integration testing in JavaScript and show you how to use Jest and Nock to write effective tests that simulate real-world scenarios.

    4 min read
  • javascript

    Unit Testing in JavaScript: How to Ensure Code Quality and Catch Bugs Early

    Unit testing is a critical aspect of software development that ensures code quality, improves maintainability, and catches bugs early. In this article, we'll explore the basics of unit testing in JavaScript, including what it is, why it's important, and how to write effective unit tests using popular testing frameworks.

    5 min read
  • javascript

    Array and Object Manipulation in JavaScript: Advanced Techniques

    Arrays and objects are fundamental data structures in JavaScript. They are used extensively in modern web development to store and manipulate data. In this blog post, we will explore advanced techniques for manipulating arrays and objects in JavaScript. We will cover topics such as flattening arrays, merging objects, filtering and mapping arrays, and much more. By the end of this post, you will have a deeper understanding of JavaScript's array and object manipulation capabilities.

    3 min read