“This is the 7th day of my participation in the Gwen Challenge in November. Check out the details: The Last Gwen Challenge in 2021.”

Iterators and Generators (1) (Iterators and Generators)

In earlier versions of ECMAScript, iterations had to be performed using loops or other helper constructs. As the amount of code increases, the code becomes more cluttered. Many languages solve this problem with native language constructs that allow the developer to iterate without having to know how to iterate in advance. The solution is the iterator pattern. Python, Java, C++, and many other languages have complete support for this pattern. JavaScript also supports the iterator pattern after ECMAScript 6.

Before ES6 introduced iterators, we used loops to perform iterations. The simplest example is as follows

for (let i = 1; i <= 10; ++i) {
console.log(i);
}
Copy the code

Performing iterations through this loop is not ideal and has the following disadvantages

  • You need to know how to use data structures before you iterate. Iterating through the data structure requires a specific knowledge of how to use the data structure[]Operator to retrieve items at a specific index position, not for all data structures.
  • The traversal order is not inherent in the data structure. The order of traversal is not inherent to The data structure. The order of traversal is not suitable for other data structures with implicit order.

The iterator pattern

The Iterator pattern (especially in the context of ECMAScript) describes a scenario in which structures can be called “iterables” because they implement the formal Iterable interface and can be consumed by the Iterator Iterator.

Iterators are disposable objects that are created on demand. Each iterator is associated with an iterable, and the iterator exposes the API that iterates over its associated iterable. An iterator does not need to know the structure of the iterable it is associated with, only how to obtain continuous values. This conceptual separation is the power of Iterable and Iterator.

An iterable is an abstract term. Basically, an iterable can be thought of as an object of collection type such as an array or a collection. However, an iterable need not be a collection object, but can be another data structure that simply behaves like an array.

Iterators are disposable objects that are created on demand. Each iterator is associated with an iterable, and the iterator exposes the API that iterates over its associated iterable. An iterator does not need to know the structure of the iterable it is associated with, only how to obtain continuous values. This conceptual separation is the power of Iterable and Iterator.

The Iterable Protocol,

Implementing the Iterable interface (an Iterable protocol) requires both capabilities:

  • Support iterative self-identification

  • The ability to create objects that implement the Iterator interface.

Implementing the Iterable interface requires both the capability to self-identify as supporting iteration and the capability to create an object that implements the Iterator interface.

In ECMAScript, this means that an attribute must be exposed as the “default iterator,” and that attribute must use a special symbol.iterator as the key. The default iterator property must refer to an iterator factory function, which must return a new iterator.

Many of the built-in types implement the Iterable interface

  • Strings
  • Arrays
  • Maps
  • Sets
  • The arguments object
  • Some DOM collection types like NodeList

A type that does not implement the Iterable interface

  • Booleans
  • Numbers
  • Objects
let num = 1;

let obj = {};

let bool = true;

// These two types do not implement iterator factory functions
console.log(num[Symbol.iterator]); // undefined
console.log(obj[Symbol.iterator]); // undefined
console.log(bool[Symbol.iterator]); // undefined

let str = 'abc';

let arr = ['a'.'b'.'c'];

let map = new Map().set('a'.1).set('b'.2).set('c'.3);

let set = new Set().add('a').add('b').add('c');

let els = document.querySelectorAll('div');

// These types all implement iterator factory functions
console.log(str[Symbol.iterator]); // f values() { [native code] }
console.log(arr[Symbol.iterator]); // f values() { [native code] } 
console.log(map[Symbol.iterator]); // f values() { [native code] } 
console.log(set[Symbol.iterator]); // f values() { [native code] } 
console.log(els[Symbol.iterator]); // f values() { [native code] }
// Calling the factory function generates an iterator
console.log(str[Symbol.iterator]()); // StringIterator {}
console.log(arr[Symbol.iterator]()); // ArrayIterator {} 
console.log(map[Symbol.iterator]()); // MapIterator {} 
console.log(set[Symbol.iterator]()); // SetIterator {} 
console.log(els[Symbol.iterator]()); // ArrayIterator {}
Copy the code

In the actual writing of the code, we don’t need to explicitly call the factory function to generate the iterator. All types that implement iterable protocols are automatically compatible with any language features that receive iterables. Native language features of iterables include:

  • for... of loop
  • Array destructuring Destructuring Array
  • The spread operator extension operator
  • Array.from()
  • Set Construction creates the collection
  • Map Construction Creates the mapping
  • Promise.all(), which expects an iterable of promises
  • Promise.race(), which expects an iterable of promises
  • The yield*operator, used in generators

These native language constructs create an iterator by calling the provided iterable’s factory function behind the scenes:

let arr = ['foo'.'bar'.'baz'];

// Array destructuring
let [a, b, c] = arr;
console.log(a, b, c);

// Spread operator
let arr2 = [...arr]; // foo, bar, baz
console.log(arr2); // ['foo', 'bar', 'baz']

// Array.from() 
let arr3 = Array.from(arr); console.log(arr3); // ['foo', 'bar', 'baz']

// Map constructor 
let pairs = arr.map((x, i) = > [x, i]); 
console.log(pairs); // [['foo', 0], ['bar', 1], ['baz', 2]] 
let map = new Map(pairs);
console.log(map); // Map(3) { 'foo'=>0, 'bar'=>1, 'baz'=>2 }If the parent class in the object's prototype chain implements the Iterable interface, then the object implements that interface:class FooArray extends Array {}
let fooArr = new FooArray('foo'.'bar'.'baz');
for (let el of fooArr) { console.log(el); } // foo // bar // baz


Copy the code

The Iterator Protocol

An iterator is a disposable object used to iterate over the iterable associated with it. The iterator API uses the next() method to traverse the data in the iterable. Each successful call to next() returns an IteratorResult object containing the next value returned by the repeater. The current position of the iterator cannot be known without calling next().

The IteratorResult object returned by the next() method contains two attributes: done and value. Done is a Boolean value indicating whether next() can be called again to get the next value; Value contains the iterable’s next value (done is false), or undefined (done is true). The done: true state is called exhaustion.

// Iterable

let arr = ['foo'.'bar'];

// Iterator factory function
console.log(arr[Symbol.iterator]); // f values() { [native code] }

/ / the iterator
let iter = arr[Symbol.iterator]();

console.log(iter); // ArrayIterator {}

// Perform iteration
console.log(iter.next()); // { done: false, value: 'foo' } 
console.log(iter.next()); // { done: false, value: 'bar' }
console.log(iter.next()); // { done: true, value: undefined }
Copy the code

Properties of iterators

  • The iterator doesn’t know how to get the next value from the iterable or how big the iterable is. As soon as the iterator reaches the done: true state, subsequent calls to next() always return the same value
  • Each iterator represents a one-time ordered traversal of the iterable. Instances of different iterators have no relation to each other and simply walk through the iterable independently
  • Iterators are not bound to a snapshot of the iterable at any point in time, but simply use cursors to record the journey through the iterable. If the iterable is modified during the iteration, the iterator is not bound to a snapshot of the iterable; it merely uses a cursor to track its progress through the iterable. If the iterable is mutated during iteration, the iterator will incorporate the changes)
let arr = ['foo'.'baz'];

let iter = arr[Symbol.iterator]();

console.log(iter.next()); // { done: false, value: 'foo' }

// Insert value in the middle of array 
arr.splice(1.0.'bar');

console.log(iter.next()); // { done: false, value: 'bar' }
console.log(iter.next()); // { done: false, value: 'baz' }
console.log(iter.next()); // { done: false, value: 'baz' }


Copy the code

Note: Iterators maintain a reference to the iterable, so iterators prevent the garbage collector from collecting the iterable

The concept of “iterator” is sometimes ambiguous because it can refer to a generic iteration, an interface, or a formal iterator type.

The term “iterator” can be somewhat nebulous because it refers to a generalized iteration concept, an interface, and formal iterator-type classes.

Custom Protocol Definitions

Like the Iterable interface, any object that implements the Iterator interface can be used as an Iterator.

class Counter {
  // Instances of Counter should iterate limit times
  constructor(limit) {
    this.limit = limit
  }
  next() {
    let count = 1,
				limit = this.limit;
    if (count <= limit) {
      return { done: false.value: count++ }
    } else {
      return { done: true.value: undefined}}} [Symbol.iterator]() {
    return this}}let counter = new Counter(3);

for (let i of counter) { console.log(i); } // 1/2/3
Copy the code

This class implements the Iterator interface, but is not ideal. This is because each instance of it can only be iterated once.

In order for an iterable to create multiple iterators, a new counter must be created for each iterator created. To do this, we can put the counter variable in a closure and then return an iterator through the closure

class Counter {
  constructor(limit) {
    this.limit = limit
  }

  [Symbol.iterator]() {
    let count = 1,
      limit = this.limit
    return {
      next() {
        if (count <= limit) {
          return { done: false.value: count++ }
        } else {
          return { done: true.value: undefined}}},}}}let counter = new Counter(3);

for (let i of counter) { console.log(i); } // 1/2/3

for (let i of counter) { console.log(i); } // 1/2/3
Copy the code

Early Termination of Iterators

The optional return() method is used to specify the logic to execute if the iterator is prematurely closed. The structure of performing iterations lets the iterator “close” when it wants to know that it does not want to iterate until the iterable runs out. Possible scenarios include:

  • A for-of loop exits prematurely with a break, continue, return, or throw;
  • The deconstruction operation does not consume all values.

The return() method must return a valid IteratorResult object. In the simple case, you can just return {done: true}.

As shown in the code below, the built-in language structure automatically calls the return() method when it finds that there are more values to iterate over but that they will not be consumed.

class Counter {
  constructor(limit) {
    this.limit = limit
  }

  [Symbol.iterator]() {
    let count = 1,
      limit = this.limit
    return {
      next() {
        if (count <= limit) {
          return { done: false.value: count++ }
        } else {
          return { done: true.value: undefined}}},return() {
        console.log('Exiting early')
        return { done: true}},}}}let counter1 = new Counter(5)

for (let i of counter1) {
  if (i > 2) {
    break
  }
  console.log(i)
}
// 1 // 2 // Exiting early
let counter2 = new Counter(5)

try {
  for (let i of counter2) {
    if (i > 2) {
      throw 'err'
    }
    console.log(i)
  }
} catch (e) {}
// 1 // 2 // Exiting early

let counter3 = new Counter(5);

let [a, b] = counter3; 
// Exiting early
Copy the code

If the iterator is not closed, you can continue iterating from where you left off last time. For example, array iterators cannot be closed

let a = [1.2.3.4.5]

let iter = a[Symbol.iterator]()

for (let i of iter) {
  console.log(i)
  if (i > 2) {
    break}}// 1/2/3

for (let i of iter) {
  console.log(i)
} / / 4 / / 5
Copy the code

Because the return() method is optional, not all iterators can be closed. To know if an iterator can be closed, we can test if the return property of the iterator instance is a function object. However, simply adding this method to an iterator that cannot be closed does not make it closed. This is because calling return() does not force the iterator into a closed state. Even so, the return() method is called anyway.

let a = [1.2.3.4.5]

let iter = a[Symbol.iterator]()

iter.return = function () {
  console.log('Exiting early')
  return { done: true}}for (let i of iter) {
  console.log(i)
  if (i > 2) {
    break}}// 1/2/3

for (let i of iter) {
  console.log(i)
} / / 4 / / 5
Copy the code

Iterator content Summary

An iterator is an interface that can be implemented by any object and supports continuous retrieval of every value produced by an object. Any object that implements the Iterable interface has a symbol. iterator property that references the default iterator. The default Iterator is like an Iterator factory, that is, a function that, when called, produces an object that implements the Iterator interface.

Iterators must be continuously evaluated by successive calls to the next() method, which returns an IteratorObject. This object contains a done attribute and a value attribute. The former is a Boolean value indicating whether there are more values to access; The latter contains the current value returned by the iterator. This interface can be consumed manually by repeatedly calling the next() method, or automatically by a native consumer such as a for-of loop. The optional return() method is used to specify the logic to execute if the iterator is prematurely closed.