DEV Community

loading...
Cover image for Master modern features of JavaScript (iterators and generators)

Master modern features of JavaScript (iterators and generators)

albert_hadacek profile image Albert Hadacek ・4 min read

Iterators and generators were introduced in 2015 as a part of the ES6 addition. We might not use them in our daily coding but behind the scenes, these features for instance allowed the implementation of destructuring, spread/rest operators, for..of loops, and async/await which are actually parts of the language we interact with every day.

Why are iterators useful?

Imagine you have an array of names and you need to print out all of the elements. The most intuitive way from a beginner's perspective is to use a classic for loop


const names = ["Jake", "Cody", "Hannah"]

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

// CONSOLE
// "Jake"
// "Cody"
// "Hannah"

Enter fullscreen mode Exit fullscreen mode

To access each element, we needed to create "an algorithm" on our own. Iterators are shifting this model and allowing us to think about data as flows of information instead of a collection.

At this point go and review closure as it's essential to understand what we are gonna do next.


const nextElement = (function(arr){
  let count = 0
  return function() {
   return arr[count++]
  }
})(["Jake", "Cody", "Hannah"])

nextElement() // "Jake"
nextElement() // "Cody"
nextElement() // "Hannah"
nextElement() // "undefined"

Enter fullscreen mode Exit fullscreen mode

Now we have the ability to iterate over an array by simply calling a function that remembers the current index and returns the correct element. This is called the iterator pattern.

Luckily for us, JavaScript has this feature built-in.


const names = ["Jake", "Cody", "Hannah"]

const it = names[Symbol.iterator]()

it.next() {value: "Jake", done: false}
//more calls ...
it.next() {value: undefined, done:  true}

Enter fullscreen mode Exit fullscreen mode

We can even define our own iterator for data types that are not iterable - objects. For..in loop does not use iterators.


const person = {
  name: "Jake",
  age: 16,
  hobby: "Annoy my twin brother",
  [Symbol.iterator]: function() {
    const keys = Object.keys(this)
    let count = 0
    return {
        next: () => (
          count < keys.length ?
          { done: false, value: this[keys[count++]] } :
          { done: true, value: undefined}
        )
    }
  }
}


console.log([...person]) // ['Jake', 16, 'Annoy my twin brother']

Enter fullscreen mode Exit fullscreen mode

In the code above we are implementing the iterator protocol on the person object, which allows us to use the spread operator on it. We designed it so when we call next(), we get the next value in the object. Notice the use of the arrow function which does not change the reference of the this keyword.

Now, we have an idea of what iterators allow us to do. So, we can formally define them: Iterators are objects that define the iterator protocol by having a next() method that returns an object with two properties (value and done).

Generators

Our implementation of the iterator works, but it is fairly complicated. To simplify that behaviour, we can use generator functions, that return an iterator object with our well-known next() method.

To declare a generator function we use the * in front of the function name. The values, that the iterator will go over are defined by the yield operator. The generator function returns for us the iterator object with the next() method.


function *generator() {
  yield "Jake"
  yield "Cody"
  yield "Hannah"
}

const it = generator()
it.next() // {value: "Jake", done: false}

console.log([...generator()]) // ["Jake", "Cody", "Hannah"]

Enter fullscreen mode Exit fullscreen mode

Now we can adjust our person object using a generator function


const person = {
  name: "Jake",
  age: 16,
  hobby: "Annoy my twin brother",
  *[Symbol.iterator]() {
    for (let key of Object.keys(this)) {
      yield this[key]
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Implementing async/await functionality from scratch

For this bit, you need to be comfortable using promises.

Before we start implementing our version of async/await, it is important to understand how to pass arguments into the next() method.


function *generator() {
  const num = yield 5
  yield num * 7
  yield 2
}

const it = generator()

it.next() // {value: 5, done: false}
it.next(3) // {value: 21, done: false}
it.next() // {value: 2, done}

Enter fullscreen mode Exit fullscreen mode

In the snippet above we can see, that the yield operator immediately returns the value 5, and when we call the next(3) the number three is placed right where the last yield appeared.

With that knowledge, we finally possess all the power to create our async/await as it's just syntactic sugar for generators used with promises.



function* asyncGenerator() {
  const data = yield fetch('someAPI/posts/1') // "AWAIT"
  console.log(data)

}

// BEHIND THE SCENES STUFF
const it = asyncGenerator()
const futureData = it.next().value

futureData
    .then((res) => res.json())
    .then((post) => it.next(post))

// CONSOLE
// {id: 1, post: "Hello there"}

Enter fullscreen mode Exit fullscreen mode

When we call it.next() for the first time we receive the Promise object as the value property, we attach the two callbacks to it which are triggered when the promise is resolved. The second callback is crucial as we finally have the post and we call next() on our iterator. The post is then passed back to the asyncGenerator and assigned to the data variable and finally, it gets printed.

The snippet above should give you an idea about what is happening behind the scenes when we use async/await.

Discussion (1)

pic
Editor guide
Collapse
mahdimomeni profile image
Mahdi Momeni

Thanks for the deep explanation