Modern JavaScript: Using Iterators
Welcome to the Using Iterators lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
We've covered a lot of background on how generators and iterators work. Now let's step back for a high-level look at them.
So far, we've primarily focused on looping over iterables with a for-of loop. But iterables are usable by more than just for-of loops. For example, array destructuring syntax will automatically work with any iterable, including custom iterables we define.
>
const letters = ['a', 'b', 'c'];const [, b, c] = letters;[b, c];Result:
['b', 'c']
>
function* letters() {yield 'a';yield 'b';yield 'c';}const [, b, c] = letters();[b, c];Result:
['b', 'c']
We can also use the spread syntax on an iterable.
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
[...letters()];Result:
['a', 'b', 'c']
By design, iterators always move forward. Once they've iterated over a value, they can never go back to that value or any earlier values. There's no
rewindorrestartmethod.At first, that seems like an annoying limitation. However, this limitation is central to the purpose of iterators. Iterators can do two things that plain arrays can't.
First, they can iterate over data even if we don't have all of the data yet. For example, if we're receiving data streaming from the network, we can write an iterator that lets us loop over it, even though only a small part of data is available to us at any given time.
Second, iterators can be infinite in length. Every computer is finite, so we can never have an infinitely large data structure. But iterators iterate over data only as it's needed, so they can represent infinite sequences of data without ever having to store it all in memory at the same time.
Iterating over streaming network data is difficult to illustrate here, but we can show an infinite iterator. For example, here's an infinite
primeNumbersiterator (starting from 2 and going up from there).>
function* primeNumbers() {let i = 2;while (true) {// Assume every number is a prime until we discover that it's not.let prime = true;/* If this number is divisible by any number less than itself, then* it's not prime. For example, 4 is divisible by 2, so it's not* prime. 5 isn't divisible by 2, 3, or 4, so it is prime.*/for (let j=2; j<i; j++) {if (i % j === 0) {prime = false;}}if (prime) {yield i;}i += 1;}}const [p1, p2, p3, p4, p5] = primeNumbers();[p1, p2, p3, p4, p5];Result:
[2, 3, 5, 7, 11]
The
primeNumbersgenerator contains an infinite loop,while(true), so it looks like it will never terminate. However, remember that generators stop executing whenever they hit ayield. They remain frozen in time until we call their iterator's.next()again, at which point they resume executing just after theyield.Above, we destructured five values from the iterator. That means that the generator ran until it had
yielded five times, then it stopped. It never restarted after that because we never called the iterator's.next()again. The loop may be infinite when examined syntactically, but eachyieldis a chance for it to pause, possibly forever.An infinite generator will go on for as long as we keep iterating. For example, we can ask
primeNumbersfor the 100th prime number by iterating over it 100 times, then stopping.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
let count = 0;let answer;for (const prime of primeNumbers()) {answer = prime;count += 1;if (count === 100) {break;}}answer;Result:
We can also write the same thing by manually calling the
next()method 100 times.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
const primes = primeNumbers();let answer;for (let i=0; i<100; i++) {answer = primes.next().value;}answer;Result:
541
The technical term for all of this is "laziness". Iterators are lazy: they only produce data when it's requested via calls to
.next(). Laziness is what allows us to iterate over data coming in from the network, or iterate over an infinite list of prime numbers.By contrast, if we did
Array.from(primeNumbers())then it would loop forever while trying to build an array with an infinite number of elements. (It would eventually run out of memory and error.)We'd see the same thing if we did
const [first, ...rest] = primeNumbers(). There, the JavaScript virtual machine would try to collect "the rest" of the numbers, but there are infinitely many. (Be careful if you try those examples in a browser's developer console: depending on your browser, it may hang the tab, the window, or the entire browser application!)Sometimes, laziness is useful even when we have a finite set of data that's stored on one computer. For example, if we want to examine 1 terabyte of data stored on the disk, but only have 128 gigabytes of RAM, then we won't be able to load it all up at once. By iterating over it lazily with an iterator, one small piece at a time, we can process the full 1 terabyte even though we never put it all in memory at once.
Here's a code problem:
Write an infinite
powersOfTwogenerator function that yields 1, 2, 4, 8, 16, etc., multiplying by 2 each time.function* powersOfTwo() {let i = 1;while (true) {yield i;i *= 2;}}const [x0, x1] = powersOfTwo();/* Get 2^31 from the iterator. */const iterator = powersOfTwo()[Symbol.iterator]();let x31;for (let i=0; i<32; i++) {x31 = iterator.next().value;}[x0, x1, x31];- Goal:
[1, 2, 2147483648]
- Yours:
[1, 2, 2147483648]
One final topic for iterators. What happens when we try to iterate over a data type that isn't actually iterable? The answer depends on exactly how we try to iterate.
Trying to use a for-of loop or destructuring incorrectly will cause an error. (You can type
errorwhen a code example will throw an error.)>
const notAnIterable = 5;for (const x of notAnIterable) {}Result:
TypeError: notAnIterable is not iterable
>
const notAnIterable = 5;const [x, y, z] = notAnIterable;[x, y, z];Result:
TypeError: notAnIterable is not iterable
However, if we call
Array.from(...)on a non-iterator value, it returns[].>
const notAnIterable = 5;Array.from(notAnIterable);Result:
[]
That's an unfortunate property of
Array.from(...). We probably won't ever useArray.fromin this way on purpose, so its behavior here can hide a bug from us. (If it threw an error, then at least we'd know that something went wrong!)That's all for iterators! They're a complex topic: there's an entire iteration protocol, and several ways to consume iterators. In exchange for their complexity, iterators let us work with streaming data, work with infinite lists of data, and use native constructs like loops with our own custom objects. And adding generators lets us avoid writing verbose iterator code, even though the iterator protocols are still at work under the hood.