JavaScript provides several ways of iterating over a collection, from simple for loops to map() and filter(). Iterators and generators bring the concept of iteration directly into the core language and provide a mechanism for customizing the behaviour of ‘for...of loops’. Iterators and generators usually come as a secondary thought when writing code, but if you can take a few minutes to think about how to use them to simplify your code, they'll save you from a lot of debugging and complexities.
Iterators
When we have an array, we typically use the ‘for loop’ to iterate over its element.
- The ‘for loop’ uses the variable 'i' to track the index of the ranks array
- The value of 'i' increments each time the loop executes as long as the value of 'i' is less than the number of elements in the ranks array. But its complexity grows when you nest a loop inside another loop.
ES6 introduced a new loop construct called ‘for...of’ to eliminate the standard loop’s complexity
The ‘for...of the loop’ can create a loop over any iterable object, not just an array.
Iterable values in JavaScript
The following values are iterable –
- Arrays
- Strings
- Maps
- Sets
Plain objects are not iterable and hence the 'for...of' uses the Symbol.iterator.
Symbol.iterator
The Symbol.iterator is a special-purpose symbol made especially for accessing an object's internal iterator. So, you could use it to retrieve a function that iterates over an array object, like so –
- An iterator is an object that can access one item at a time from a collection while keeping track of its current position
- It just requires that you have a method called next() to move to the next item to be a valid iterator
- The result of next() is always an object with two properties –
- Value: The value in the iteration sequence
- Done: true | false
Generators
Generator functions once called, returns the Generator object, which holds the entire Generator iterable and can be iterated using next() method. Every next() call on the generator executes every line of code until it encounters the next yield and suspends its execution temporarily.
Generators are a special type of function in JavaScript that can pause and resume state. A Generator function returns an iterator, which can be used to stop the function in the middle, do something, and then resume it whenever.
- This generator object needs to be assigned to a variable to keep track of the subsequent next() methods called on itself.
- If the generator is not assigned to a variable then it will always yield only till the first yield expression on every next().
- A generator function is a function marked with the * and has at least one yield-statement in it.
- Syntactically they are identified with a *, either function* X or function *X, — both mean the same thing
Fun Fact – async/await can be based on generators
Generator functions/yield and Async functions/await can both be used to write asynchronous code that 'waits', which means code that looks as if it was synchronous, even though it is asynchronous. ... An async function can be decomposed into a generator and promise implementation which is good to know stuff.
Generator functions are written using the function* syntax –
- ‘function*’ is a new 'keyword' for generator functions
- yield is an operator with which a generator can pause itself
Additionally, generators can also receive input and send output via yield. In short, a generator appears to be a function but it behaves like an iterator.
A generator is a function that returns an object on which you can call next(). Every invocation of next() will return an object of shape —
- The value property will contain the value
- The done property is either true or false
- When the done becomes true, the generator stops and won’t generate any more values
Here are some other common definitions of generators
- Generators are a special class of functions that simplify the task of writing iterators
- A generator is a function that produces a sequence of results instead of a single value, i.e you generate a series of values
- The value property will contain the value. The done property is either true or false. When the done becomes true, the generator stops and won’t generate any more values. The yield is a magical keyword that can do more things other than simply return a value and next() can do more things aside from retrieving the value.
- A passing argument to next() - The argument passed to next() will be received by yield –
Output
Passing a function to yield
Apart from returning values, the yield can also call a function –
Output
Delegating to another generator or iterable using yield* expression
Output
Let’s see an example of fetching a single value from an API
As a Title –
Output
- In this example; to fetch data from API, we have to install node-fetch using the command –
'npm install node-fetch'
- We then pass a generator to a function as a parameter. Let's call the function getTitle()
- Now, in the function, we have to go through some steps to execute the generator
- Initially, we will call the generator method. It returns an iterator(object) which is caught in a variable 'iterator'
- Now execute the iterator using 'next()' method
- When the next method is called, the generator starts executing from this point. At line 4 the 'URL' is fetched
- Fetch returns an object which is captured into variable 'iteration'
- The object has 2 fields, viz. 'value' and 'done'
- Here value is a promise and done is a boolean set to false
- Iteration has a promise. Here we used our first yield
- In our case, the getTitle() function has to resolve the promise. Since the yield doesn’t know how to resolve the promise. So for resolving that promise we caught iteration.value into a variable 'promise'
- Now we resolve the promise say into variable 'x'
- We send the resolved 'x' to the iterator's next method. This x is a response which we collect into a variable 'response'. (line 5)
- Now we have to extract the post from the response
- The response object now again has a promise which is to be resolved by our function. So we extract the object into variable 'anotherIterator' and the value of that object viz. promise into 'anotherPromise'
- Now we resolve that promise in 'y' and pass it to the generator through the next() method
- Here we used the second yield. So now we have resolved the response.json() and caught it into variable 'post'. (line 6)
- Finally, we get our title through the object 'post'.Now we are going to extract our title from 'post.title'. Check the console for the title
Advantages of Generators
Lazy Evaluation
This is an evaluation model that delays the evaluation of an expression until its value is needed. That is, if the value is not needed, it will not exist. It is calculated on demand.
Memory Efficient
A direct outcome of Lazy Evaluation is that generators are memory efficient.
The only values generated are those that are needed. With normal functions, all the values must be pre-generated and kept around case they need to be used later.
Conclusion
We have learned the following things about iterators and generators –
- Iterator functions are a great and efficient way to do a lot of things in JavaScript. There are many other possible ways of using a generator function
- Keeping iteration logic with the object it belongs to, is a good practice and which is the focus of ES6 features
- The ability of a function to exchange data with the calling code during the execution is unique. And, surely, they are great for making iterable objects
- As can be evidenced by the examples, generators are a really powerful tool that lets you have cleaner code - especially when it comes to any kind of asynchronous behaviour