The Power of Iterator Design Pattern in JavaScript

Christopher T.

May 28th, 2022

Share This Post

In almost every single JavaScript application we write there is always some form of looping through collections. Looping collections becomes necessary when we receive data through an http request for example.

Since data can take on many forms not every algorithm in looping over them is able to get the job done quickly and/or efficiently than other looping methods.

In this case there's an obvious a problem: We need to be able to switch between different algorithms of iterating through collections whenever we need to. A design pattern that solves this specific problem is called the Iterator Design Pattern.

With that said, in this post we will be going over the Iterator Design Pattern in JavaScript and implement one hands-on. We will learn why a different algorithm becomes necessary when developing.

How The Iterator Pattern Visually Looks Like

This is how the pattern looks like in a visual perspective:

iterator-design-pattern-in-javascript-diagram-flow.png

Implementation

Lets pretend we have an array arr which is a collection of numbers:

const arr = [3, 10, 11, 11.1, 4, 0, 1, -1, 200, 108, 10, 0]

If we we're developing a program and we have a commonly used function that takes in an list of values and a callback which returns a value that we're looking for using the callback as the source of truth, one of the most obvious readily available strategies we can use is the for loop:

function findValue(arr, callback) {
  for (let index = 0; index < arr.length; index++) {
    const value = arr[index]
    if (callback(value)) {
      return value
    }
  }
}

Or we can use the Iterator Pattern and instead work with different variations of iterations treating them as injectable algorithms:

function createIterator(getIter) {
  return function getIterator() {
    let collection = this.collection
    let index = collection.length
    let iter = getIter(collection)

    return {
      next: iter.next,
      get index() {
        return 'index' in iter ? iter.index : index
      },
    }
  }
}

function makeTraverser() {
  return {
    traverse(collection) {
      this.collection = collection
      for (const value of this) console.log(value)
    },
    use(iter) {
      this[Symbol.iterator] = createIterator(iter)
      return this
    },
  }
}

The reason why it is better to abstract out the implementation details is because collections can come in various data structures. Some data structures can be traversed more efficiently with alternate algorithms. We can allow our objects to be able to switch from one iterator to another in the runtime whenever there are more sufficient algorithms to finish the task.

Let's continue on with our example and create a simple iterator:

const traverser = makeTraverser()

traverser.use(function iterator(items) {
  return {
    next() {
      return {
        get value() {
          return items.pop()
        },
        get done() {
          return !items.length
        },
      }
    },
  }
})

We're able to loop through the collection and see the results:

const arr = [3, 10, 11, 11.1, 4, 0, 1, -1, 200, 108, 10, 0]
traverser.traverse(arr)

iterator-design-pattern-iterator-result.png

What if we had a nested collection of items? Our current iteration method is no longer efficient because it doesn't account for key values that are deeper in depth:

const elems = [
  { tagName: 'div', style: { width: '28.5px', height: '20px' } },
  { tagName: 'label', style: { width: '28.5px', height: '20px' } },
  {
    tagName: 'div',
    style: { width: '28.5px', height: '20px' },
    children: [
      {
        tagName: 'div',
        style: { width: '28.5px', height: '20px' },
        children: [
          { tagName: 'input', style: { width: '28.5px', height: '20px' } },
          { tagName: 'input', style: { width: '28.5px', height: '20px' } },
          { tagName: 'select', style: { width: '28.5px', height: '20px' } },
          {
            tagName: 'div',
            style: { width: '28.5px', height: '20px' },
            children: [
              { tagName: 'input', style: { width: '28.5px', height: '20px' } },
              {
                tagName: 'div',
                style: { width: '28.5px', height: '20px' },
                children: [
                  {
                    tagName: 'a',
                    href: 'https://google.com',
                    target: '_blank',
                  },
                ],
              },
              { tagName: 'select', style: { width: '28.5px', height: '20px' } },
            ],
          },
          { tagName: 'input', style: { width: '28.5px', height: '20px' } },
        ],
      },
    ],
  },
]

traverser.traverse(elems)

iterator-design-pattern-logging-elems.png

Collections of data can become very complex and they're not always just plain JSON objects. Programs in JavaScript often fetch data structures, transform them and only make them publicly available to the outside with functions to work with to do common tasks like generate a final collection of data.

This is why the Iterator pattern is important as it promotes extensibility, scalability, and composition.

With that said we can use a different algorithm to get all descendants of our elems collection:

traverser.use(function iterator(items) {
  items = [...items]

  const getItems = (_items) => {
    const nextItem = _items.pop()
    if (nextItem.children) _items.unshift(...nextItem.children)
    return nextItem
  }

  return {
    next() {
      return {
        get value() {
          return getItems(items)
        },
        get done() {
          return !items.length
        },
      }
    },
  }
})

better-iterator-design-pattern-logging-elems-result.png

I'm sure you have noticed the syntax Symbol.iterator. JavaScript uses this to place the default iterator for any object we attach this to. Don't be confused and assume that this is part of the pattern. It's just syntax sugar. We are simply taking advantage of this syntax to enable the for of to operate which is something entirely related to JavaScript.

Let's hop straight back into the Iterator Pattern. There's not really much of a useful use case shown in these examples yet to see how powerful it can become but we did go through an issue it solved in a practical use case.

So let's extend our examples even further to add more control in how we want to work with collections because understanding the next parts is crucial to understand how this pattern really helps us.

What we have so far is a nested collection of representational DOM objects. We exposed one issue we had when using a regular for loop where it wasn't enough to cover deep collections so we implemented a variation of the Iterator Pattern to satisfy our needs.

However it would be very helpful to have the ability in each iteration to be able to see where we currently are in complex data structures.

Lets pretend we want access to the current item (which we currently do), the current depth we are in (each access to an element's children increases depth), the current index of the collection, the previous as well as the next item in the collection, the previous, current and next element in the iteration (this is not the same as the current collection), and what the items in the iteration currently looks like.

With all of this information we're able to do pretty much anything we want to with our collection of elements.

This is how we can implement that:

function createIterator(getIter) {
  return function getIterator() {
    let collection = this.collection
    let items = [...collection]
    let iter = getIter(collection)
    let depth = 0
    let index = 0
    let currentItem = null
    let currentElement = null
    let previousItem = null
    let previousElement = null
    let nextItem = null
    let nextElement = null

    const next = () => {
      previousItem = currentItem
      currentItem = collection[index]
      nextItem = collection[++index] || null

      if (currentElement?.children) {
        depth++
        items.unshift(...currentElement.children)
      }

      previousElement = currentElement
      currentElement = items.pop() || null
      nextElement = items[0] || null

      return {
        previousItem,
        previousElement,
        currentItem,
        currentElement,
        nextItem,
        nextElement,
        depth,
        index,
        items,
      }
    }

    return {
      get next() {
        return iter.next(next)
      },
    }
  }
}

function makeTraverser() {
  return {
    traverse(collection, callback) {
      this.collection = collection
      for (const value of this) callback(value)
    },
    use(iter) {
      this[Symbol.iterator] = createIterator(iter)
      return this
    },
  }
}

const traverser = makeTraverser()

traverser.use(function iterator(items) {
  return {
    next: (next) => () => {
      const props = next()
      return {
        get value() {
          return props
        },
        get done() {
          return !props.items.length
        },
      }
    },
  }
})

All we need to do now is to invoke it and provide a callback. Our callback will be provided all of this information on each iteration:

traverser.traverse(elems, function callback(args) {
  console.log(args)
})

Results:

[
  {
    "previousItem": null,
    "previousElement": null,
    "currentItem": {
      "tagName": "div",
      "style": { "width": "28.5px", "height": "20px" }
    },
    "currentElement": {
      "tagName": "div",
      "style": { "width": "28.5px", "height": "20px" },
      "children": [
        {
          "tagName": "div",
          "style": { "width": "28.5px", "height": "20px" },
          "children": [
            {
              "tagName": "input",
              "style": { "width": "28.5px", "height": "20px" }
            },
            {
              "tagName": "input",
              "style": { "width": "28.5px", "height": "20px" }
            },
            {
              "tagName": "select",
              "style": { "width": "28.5px", "height": "20px" }
            },
            {
              "tagName": "div",
              "style": { "width": "28.5px", "height": "20px" },
              "children": [
                {
                  "tagName": "input",
                  "style": { "width": "28.5px", "height": "20px" }
                },
                {
                  "tagName": "div",
                  "style": { "width": "28.5px", "height": "20px" },
                  "children": [
                    {
                      "tagName": "a",
                      "href": "https://google.com",
                      "target": "_blank"
                    }
                  ]
                },
                {
                  "tagName": "select",
                  "style": { "width": "28.5px", "height": "20px" }
                }
              ]
            },
            {
              "tagName": "input",
              "style": { "width": "28.5px", "height": "20px" }
            }
          ]
        }
      ]
    },
    "nextItem": {
      "tagName": "label",
      "style": { "width": "28.5px", "height": "20px" }
    },
    "nextElement": {
      "tagName": "div",
      "style": { "width": "28.5px", "height": "20px" }
    },
    "depth": 0,
    "index": 1,
    "items": []
  },
  {
    "previousItem": {
      "tagName": "div",
      "style": { "width": "28.5px", "height": "20px" }
    },
    "previousElement": {
      "tagName": "div",
      "style": { "width": "28.5px", "height": "20px" },
      "children": [
        {
          "tagName": "div",
          "style": { "width": "28.5px", "height": "20px" },
          "children": [
            {
              "tagName": "input",
              "style": { "width": "28.5px", "height": "20px" }
            },
            {
              "tagName": "input",
              "style": { "width": "28.5px", "height": "20px" }
            },
            {
              "tagName": "select",
              "style": { "width": "28.5px", "height": "20px" }
            },
            {
              "tagName": "div",
              "style": { "width": "28.5px", "height": "20px" },
              "children": [
                {
                  "tagName": "input",
                  "style": { "width": "28.5px", "height": "20px" }
                },
                {
                  "tagName": "div",
                  "style": { "width": "28.5px", "height": "20px" },
                  "children": [
                    {
                      "tagName": "a",
                      "href": "https://google.com",
                      "target": "_blank"
                    }
                  ]
                },
                {
                  "tagName": "select",
                  "style": { "width": "28.5px", "height": "20px" }
                }
              ]
            },
            {
              "tagName": "input",
              "style": { "width": "28.5px", "height": "20px" }
            }
          ]
        }
      ]
    },
    "currentItem": {
      "tagName": "label",
      "style": { "width": "28.5px", "height": "20px" }
    },
    "currentElement": {
      "tagName": "label",
      "style": { "width": "28.5px", "height": "20px" }
    },
    "nextItem": {
      "tagName": "div",
      "style": { "width": "28.5px", "height": "20px" },
      "children": [
        {
          "tagName": "div",
          "style": { "width": "28.5px", "height": "20px" },
          "children": [
            {
              "tagName": "input",
              "style": { "width": "28.5px", "height": "20px" }
            },
            {
              "tagName": "input",
              "style": { "width": "28.5px", "height": "20px" }
            },
            {
              "tagName": "select",
              "style": { "width": "28.5px", "height": "20px" }
            },
            {
              "tagName": "div",
              "style": { "width": "28.5px", "height": "20px" },
              "children": [
                {
                  "tagName": "input",
                  "style": { "width": "28.5px", "height": "20px" }
                },
                {
                  "tagName": "div",
                  "style": { "width": "28.5px", "height": "20px" },
                  "children": [
                    {
                      "tagName": "a",
                      "href": "https://google.com",
                      "target": "_blank"
                    }
                  ]
                },
                {
                  "tagName": "select",
                  "style": { "width": "28.5px", "height": "20px" }
                }
              ]
            },
            {
              "tagName": "input",
              "style": { "width": "28.5px", "height": "20px" }
            }
          ]
        }
      ]
    },
    "nextElement": {
      "tagName": "div",
      "style": { "width": "28.5px", "height": "20px" },
      "children": [
        {
          "tagName": "input",
          "style": { "width": "28.5px", "height": "20px" }
        },
        {
          "tagName": "input",
          "style": { "width": "28.5px", "height": "20px" }
        },
        {
          "tagName": "select",
          "style": { "width": "28.5px", "height": "20px" }
        },
        {
          "tagName": "div",
          "style": { "width": "28.5px", "height": "20px" },
          "children": [
            {
              "tagName": "input",
              "style": { "width": "28.5px", "height": "20px" }
            },
            {
              "tagName": "div",
              "style": { "width": "28.5px", "height": "20px" },
              "children": [
                {
                  "tagName": "a",
                  "href": "https://google.com",
                  "target": "_blank"
                }
              ]
            },
            {
              "tagName": "select",
              "style": { "width": "28.5px", "height": "20px" }
            }
          ]
        },
        {
          "tagName": "input",
          "style": { "width": "28.5px", "height": "20px" }
        }
      ]
    },
    "depth": 1,
    "index": 2,
    "items": []
  },
  {
    "previousItem": {
      "tagName": "label",
      "style": { "width": "28.5px", "height": "20px" }
    },
    "previousElement": {
      "tagName": "label",
      "style": { "width": "28.5px", "height": "20px" }
    },
    "currentItem": {
      "tagName": "div",
      "style": { "width": "28.5px", "height": "20px" },
      "children": [
        {
          "tagName": "div",
          "style": { "width": "28.5px", "height": "20px" },
          "children": [
            {
              "tagName": "input",
              "style": { "width": "28.5px", "height": "20px" }
            },
            {
              "tagName": "input",
              "style": { "width": "28.5px", "height": "20px" }
            },
            {
              "tagName": "select",
              "style": { "width": "28.5px", "height": "20px" }
            },
            {
              "tagName": "div",
              "style": { "width": "28.5px", "height": "20px" },
              "children": [
                {
                  "tagName": "input",
                  "style": { "width": "28.5px", "height": "20px" }
                },
                {
                  "tagName": "div",
                  "style": { "width": "28.5px", "height": "20px" },
                  "children": [
                    {
                      "tagName": "a",
                      "href": "https://google.com",
                      "target": "_blank"
                    }
                  ]
                },
                {
                  "tagName": "select",
                  "style": { "width": "28.5px", "height": "20px" }
                }
              ]
            },
            {
              "tagName": "input",
              "style": { "width": "28.5px", "height": "20px" }
            }
          ]
        }
      ]
    },
    "currentElement": {
      "tagName": "div",
      "style": { "width": "28.5px", "height": "20px" }
    },
    "nextItem": null,
    "nextElement": {
      "tagName": "div",
      "style": { "width": "28.5px", "height": "20px" },
      "children": [
        {
          "tagName": "input",
          "style": { "width": "28.5px", "height": "20px" }
        },
        {
          "tagName": "input",
          "style": { "width": "28.5px", "height": "20px" }
        },
        {
          "tagName": "select",
          "style": { "width": "28.5px", "height": "20px" }
        },
        {
          "tagName": "div",
          "style": { "width": "28.5px", "height": "20px" },
          "children": [
            {
              "tagName": "input",
              "style": { "width": "28.5px", "height": "20px" }
            },
            {
              "tagName": "div",
              "style": { "width": "28.5px", "height": "20px" },
              "children": [
                {
                  "tagName": "a",
                  "href": "https://google.com",
                  "target": "_blank"
                }
              ]
            },
            {
              "tagName": "select",
              "style": { "width": "28.5px", "height": "20px" }
            }
          ]
        },
        {
          "tagName": "input",
          "style": { "width": "28.5px", "height": "20px" }
        }
      ]
    },
    "depth": 1,
    "index": 3,
    "items": []
  }
]

Conclusion

And that concludes the end of this post! I hope you have found valuable information here and look out for more from me in the future!


Top online courses in Web Development

Tags


Read every story from jsmanifest (and thousands of other writers on medium)

Your membership fee directly supports jsmanifest and other writers you read. You'll also get full access to every story on Medium.

Subscribe to the Newsletter

Get continuous updates

Mediumdev.toTwitterGitHubrss

© jsmanifest 2023