fbpx logo-new mail facebook Dribble Social Icon Linkedin Social Icon Twitter Social Icon Github Social Icon Instagram Social Icon Arrow_element diagonal-decor rectangle-decor search arrow circle-flat
Development

How Do JavaScript Iterables Work?

Cain Watson Engineering

Introduction

In JavaScript, we have access to many data types (Strings, Arrays, Sets, and Maps) that store collections of data. We also have useful features of the language such as the for…of loop and …spread that help us iterate over them and access each item they contain.

const fruitString = 'Pineapple'
const fruits = ['🍎', '🍍', '🍌']

for (const char of fruitString) {
   console.log(char)
}
// prints:
// 'P'
// 'i'
// 'n'
// 'e'
// 'a'
// 'p'
// 'p'
// 'l'
// 'e'

for (const fruit of fruits) {
   console.log(fruit)
}
// prints:
// '🍎'
// '🍍'
// '🍌'

What do these have in common that makes for…of work with them? Is it possible to create custom objects that work with for…of too?

  1. They are all iterables
  2. Yes you can, but first we must learn what exactly is an iterable πŸ€”

What is an iterable?

An β€œiterable” is any object that meet the following criteria:

  • Contains a property with the key Symbol.iterator set to a function
  • The iterator function returns an iterator
  • The iterator returned is an object with next function
  • The iterator’s next function returns an object with done and value properties

Many built in data types like strings implement this contract, let’s see that in action:

const string = 'abcd'
const stringIterator = string[Symbol.iterator]()

stringIterator.next() // { done: false, value: 'a' }
stringIterator.next() // { done: false, value: 'b' }
stringIterator.next() // { done: false, value: 'c' }
stringIterator.next() // { done: false, value: 'd' }
stringIterator.next() // { done: true, value: undefined }

If you’re not familiar with Symbols, they’re a primitive data type for creating unique values. We’re required to use the iterator symbol because that’s what consumers of the iterator (like for…of) will look for.

Each time .next is called, we iterate through the sequence of characters in the string. Once we’ve iterated through the entire sequence, the done property comes back as true.

How can I create my own iterable?

We’ve seen how we can use a builtin iterables like strings, but we can also create our own iterables by implementing this contract ourselves:

const iterable = {
   [Symbol.iterator]: () => {
      const iterator = {
         next() {
            return { done: false, value: '🍍' }
         },
      }
      return iterator
   },
}
const iterator = iterable[Symbol.iterator]()

iterator.next() // { done: false, value: '🍍' }
iterator.next() // { done: false, value: '🍍' }
iterator.next() // { done: false, value: '🍍' }

We’ve successfully created an iterable that endlessly gives us pineapples! Technically, we can use the for…of loop on our iterable, but it would create an infinite loop!

If we create a count variable and increment it each time .next is called, we can then return { done: true } after 3 iterations.

const iterable = {
   [Symbol.iterator]: () => {
      let count = 0
      const iterator = {
         next() {
            count++
            if (count > 3) {
               return { done: true }
            }
            return { done: false, value: '🍍'.repeat(count) }
         },
      }
      return iterator
   },
}
const iterator = iterable[Symbol.iterator]()

iterator.next() // { done: false, value: '🍍' }
iterator.next() // { done: false, value: '🍍🍍' }
iterator.next() // { done: false, value: '🍍🍍🍍' }
iterator.next() // { done: true }

Iterable Sugar

Now that our iterable ends, we can use a for…of loop to have it do all the work of calling .next for us until the sequence completes.

for (const value of iterable) {
   console.log(value)
}
// prints:
// '🍍'
// '🍍🍍'
// '🍍🍍🍍'

We can also use the spread syntax!

const basket = ['🍎', ...iterable, '🍌']
// ['🍎', '🍍', '🍍🍍', '🍍🍍🍍', '🍌']

Generators

Making our own custom iterables was pretty neat, but we can also use JavaScript Generators to create custom iterables!

A generator is a special type of function that provides an alternative syntax for creating iterators:

  • They must use the function keyword (cannot be an arrow function)
  • They must have an asterisk after the function keyword (function* funk() {} or function *funk() {})
  • They implicitly return a generator, which follows the same rules as iterators (have next function)
  • Values are added to the generator sequence by using the yield syntax
function *fruitGenerator() {
   yield '🍎'
   yield '🍍'
   yield '🍌'
   return 'final value'
}

// Cycle through generator with .next
const generator = fruitGenerator()
generator.next() // { done: false, value: '🍎' }
generator.next() // { done: false, value: '🍍' }
generator.next() // { done: false, value: '🍌' }
generator.next() // { done: true, value: 'final value' }

// Since generators are a type of iterator, we can also use for...of and spread with them!
for (const fruit of fruitGenerator()) {
   console.log(fruit)
}
// prints:
// '🍎'
// '🍍'
// '🍌'

// Using spread
const basket = ['πŸ‡', ...fruitGenerator()]
// ['πŸ‡', '🍎', '🍍', '🍌']

Async Iterables

Iterables are cool, but what happens if we need to perform some asynchronous code to fetch the next value in the sequence? Enter async iterables.

Async iterables are iterables that work with Promises:

  • They still require a property for creating an iterator, but this time they use a slightly different key: Symbol.asyncIterator
  • That key is set to a function that returns an async iterator
  • The async iterator is an object with a .next function
  • The async iteator’s next function returns a Promise
  • That promise resolves to an object with done and value properties
// Wait one second and then resolve with requested amount of pineapples
function fetchPineapples(amount) {
   return new Promise((resolve) => {
      setTimeout(() => {
         resolve('🍍'.repeat(amount))
      }, 1000)
   })
}

const iterable = {
   [Symbol.asyncIterator]: () => {
      let count = 0
      const asyncIterator = {
         async next() {
            count++
            if (count > 3) {
               return { done: true }
            }
            const pineapples = await fetchPineapples(count)
            return { done: false, value: pineapples }
         },
      }
      return asyncIterator
   }
}
const asyncIterator = iterable[Symbol.asyncIterator]()

await asyncIterator.next() // waits 1s, then resolves to { done: false, value: '🍍' }
await asyncIterator.next() // waits 1s, then resolves to { done: false, value: '🍍🍍' }
await asyncIterator.next() // waits 1s, then resolves to { done: false, value: '🍍🍍🍍' }
await asyncIterator.next() // immediately resolves to { done: true }

We can use a for await...of loop with async iterators:

for await (const value of asyncIterator()) {
   console.log(value)
}
// waits 1s, then prints '🍎'
// waits 1s, then prints '🍍'
// waits 1s, then prints '🍌'

Async Generators

And of course, there’s a counterpart for performing async code with generators!

An async generator is a generator function that allows you to use async/await to perform asynchronous actions before yielding the next value in the sequence.

async function* fruitGenerator() {
   // Pretend these fetchers exist and make some asynchronous call to fetch data
   const apple = await fetchApple()
   yield apple

   const pineapple = await fetchPineapple()
   yield pineapple

   const banana = await fetchBanana()
   yield banana
}

Conclusion

That concludes our walkthrough of iterables in JavaScript. We learned what they are, how to use them, and how to make our own manually or using a generator. We also learned how we can use asynchronous code with iterables through async iterables and async generators. Happy iterating!

Let’s do something great together

We do our best work in close collaboration with our clients. Let’s find some time for you to chat with a member of our team.

Say Hi