Created
March 26, 2021 14:27
Revisions
-
waynebaylor created this gist
Mar 26, 2021 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,187 @@ # Iterators An iterator provides a way to iterate over a collection of values. One of the most common uses for an iterator is in [for...of loops](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of). A for...of loop will loop over the values given to it, but how does it know what order to use? For example, why does this for...of loop start at index 0? Why not start at the end and go backwards? ```typescript const arrayOfFruits = ['apple', 'orange', 'banana', 'pear']; for (const val of arrayOfFruits) { console.log(val); } // Expected output: // apple // orange // banana // pear ``` The answer is that `Array` has a built-in iterator and it's implemented to start from index 0 and move forward through the array. While that's a useful way to iterate over the elements of an array, it isn't the _only_ way to iterate over them. We could implement our own iterator that chooses a different order. To implement a custom iterator there are some [rules you must follow](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterator_protocol). All iterators must implement an interface like: ```typescript interface Iterator { next: () => { done: boolean; value: any; }; } ``` Here is an iterator that iterates through an array backwards: ```typescript class BackwardsIterator { values: any[]; index: number; constructor(values: any[]) { this.values = values; this.index = values.length; } next() { if (this.index - 1 < 0) { return { done: true }; } this.index--; return { done: false, value: this.values[this.index], }; } } ``` You can see it in action by repeatedly calling its `next()` method: ```typescript const arrayOfFruits = ['apple', 'orange', 'banana', 'pear']; const backwardsIter = new BackwardsIterator(arrayOfFruits); let result = backwardsIter.next(); while (!result.done) { console.log(result.value); result = backwardsIter.next(); } // Expected output: // pear // banana // orange // apple ``` # Iterable Now that we have a custom iterator, let's see how we can get it to work with for...of loops. When we pass a collection or object to a for...of loop it will look for a [property that returns an iterator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol). Specifically it will look for the special property `[Symbol.iterator]` that when executed should return an iterator. Objects with this property are called **iterable**. Iterables must implement an interface like: ```typescript interface Iterable { [Symbol.iterator]: () => Iterator; } ``` Let's create our own iterable class that holds a list of words and uses `BackwardsIterator`: ```typescript class WordList { words: string[] = []; add(word: string) { this.words.push(word); } [Symbol.iterator]() { return new BackwardsIterator(this.words); } } ``` By giving `WordList` the special method `[Symbol.iterator]` we've made it iterable and you can use it directly in for...of loops. Another important detail is that we return a new iterator each time `[Symbol.iterator]` is called. This allows you to iterate as many times as you want without those iterators interfering with each other. ```typescript const places = new WordList(); places.add('New York'); places.add('Paris'); places.add('Mexico City'); for (const p of places) { console.log(p); } // Expected output: // Mexico City // Paris // New York ``` # Generators Implementing iterators and iterables can be verbose sometimes, but luckily the language provides an alternative syntax that makes it easier to express the behavior we want. Instead of writing iterables that provide iterators (like above) we can create something called a **generator** which is both an iterable _and_ an iterator. Generators also give us some special syntax for returning values. The following generator function does the same thing as our `BackwardsIterator` and `WordList` classes: ```typescript function* backwardsGeneratorFunction(values: any[]) { let index = values.length; while (index - 1 >= 0) { index--; yield values[index]; } } ``` Notice the `*` in `function*` and the use of the `yield` keyword. Generator functions are declared with `function*`, which means you can't make an arrow function a generator function. The `yield` keyword is how a generator "returns" values to the caller and can only be used inside a generator function (just like `await` can only be used inside an `async` function). When you execute a generator function it returns a **generator**. ```typescript const powersOfTwo = [2, 4, 8, 16, 32]; const backwardsGenerator = backwardsGeneratorFunction(powersOfTwo); // backwardsGenerator is an iterator console.log(typeof backwardsGenerator.next); // Expected output: // function // backwardsGenerator is also iterable console.log(typeof backwardsGenerator[Symbol.iterator]); // Expected output: // function console.log(backwardsGenerator.next()); console.log(backwardsGenerator.next()); // Expected output: // {value: 32, done: false} // {value: 16, done: false} ``` Generators are special in that they pause execution when they get to a `yield` statement. We called `next()` twice and each time the generator ran until it hit `yield`, returned the yielded value, and then paused. Since a generator is both an iterable and an iterator we can use it directly in for...of loops: ```typescript const powersOfTwo = [2, 4, 8, 16, 32]; const backwardsGenerator = backwardsGeneratorFunction(powersOfTwo); for (const val of backwardsGenerator) { console.log(val); } // Expected output: // 32 // 16 // 8 // 4 // 2 ```