JavaScript Advanced Programming (4th edition) Reading Notes

Chapter 7 _ Iterators and Generators

This paper mainly

This chapter focuses on iterators and generators in EAM. To put it simply: iterators are iterables, and generators are functions that create iterators. Iterators have the symbol. iterator method and the next() method, which can be passed by the for.. Of replaces the ordinary for loop to iterate, eliminating loop reference variables and simplifying the loop process. Generator functions have the yield keyword inside to provide the pause interface as a node to be executed by the next() method called by the created iterator. Generator functions differ from normal functions in that they have an asterisk (*) after the function keyword.

7.1 Understanding Iterations

In JavaScript, a counting loop is a simple iteration:

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

Loops are the basis of the iterative mechanism because they specify the number of iterations and what to do with each iteration. Each loop is completed before the next iteration begins, in a predefined order. The iteration takes place over an ordered set. (” ordered “can be understood to mean that all items in the set can be traversed in a given order, especially when the beginning and end items are clearly defined.) Arrays are a classic example of an ordered collection in JavaScript.

let collection = ['foo'.'bar'.'baz'];  
 
for (let index = 0; index < collection.length; ++index) { 
  console.log(collection[index]);
}
Copy the code

Because the array has a known length and each item can be retrieved by index, the entire array can be traversed by incrementing the index.

Executing routines through this loop is not ideal for the following reasons.

  • You need to know how to use data structures before you iterate. Each item in an array can be retrieved only by reference to the array object and then by the [] operator at a specific index position. This does not apply to all data structures.
  • The traversal order is not inherent in the data structure. Increasing the index to access data is an array type specific approach that does not apply to other data structures with implicit order.

ES5 has added the array.prototype.foreach () method, which is a step toward the generic iteration requirement (but still not ideal) :

let collection = ['foo'.'bar'.'baz']; 
 
collection.forEach((item) = > console.log(item)); 
// foo // bar // baz
Copy the code

This approach solves the problem of logging indexes separately and fetching values from array objects. However, there is no way to identify when an iteration ends. So this method only works with arrays, and the callback structure is clunky.

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. This 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.

7.2 Iterator mode

The Iterator pattern (especially in the context of ECMAScript) describes a scenario in which some structures can be called “iterables” because they implement the formal Iterable interface and can be consumed through the Iterator 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. They all contain finite elements, and all have an unambiguous traversal order:

// The elements of the array are finite
// Incrementing indexes can access each element in order
let arr = [3.1.4]; 
 
// Set elements are finite
// Each element can be accessed in insertion order
let set = new Set().add(3).add(1).add(4); 
Copy the code

However, an iterable does not have to be a collection object. It can also be another data structure that simply behaves like an array, such as the counting loop mentioned at the beginning of this chapter. The values generated in the loop are transient, but the loop itself performs an iteration. Counting loops and arrays both have the behavior of an iterable.

Any data structure implementing the Iterable interface can be consumed by the structure implementing the Iterator interface. 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.

7.2.1 Iterable protocol

Implementing the Iterable interface (the Iterable protocol) requires both the ability to self-identify to support iteration and the ability to create objects that implement the Iterator interface. In ECMAScript, this means that a property must be exposed as the “default iterator,” and that property 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:

  • string
  • An array of
  • mapping
  • A collection of
  • The arguments object
  • DOM collection type such as NodeList

Check if there is a default iterator attribute that exposes the factory function:

let num = 1; let obj = {}; 
 
// These two types do not implement iterator factory functions
console.log(num[Symbol.iterator]); // undefined 
console.log(obj[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

You don’t need to explicitly call this factory function to generate iterators when you actually write code. All types that implement iterable protocols are automatically compatible with any language features that receive iterables. Native language features for receiving iterables include:

  • For – loops
  • An array of deconstruction
  • Extension operator
  • Array.from()
  • Create a collection
  • Create a mapping
  • Promise.all() receives an iterable made up of terms
  • Promise.race() receives an iterable made up of terms
  • Yield * operator, used in generators

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

let arr = ['foo'.'bar'.'baz']; 
 
/ / the for - cycle
for (let el of arr) {
  console.log(el);
} // foo // bar // baz 

// Array destruct
let [a, b, c] = arr;
console.log(a, b, c); // foo, bar, baz 
 
// Extend the operator
let arr2 = [...arr];
console.log(arr2); // ['foo', 'bar', 'baz'] 
 
// Array.from() 
let arr3 = Array.from(arr);
console.log(arr3); // ['foo', 'bar', 'baz'] 
 
// Set constructor
let set = new Set(arr);
console.log(set); // Set(3) {'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 } 

Copy the code

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

7.2.2 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 iterator. 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 next value of the iterable (done is false), or undefined (done is true). The done: true state is called “exhausted”. This can be demonstrated with the following simple array:

// 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 } 
console.log(iter.next()); // { done: true, value: undefined } 
console.log(iter.next()); // { done: true, value: undefined } 
console.log(iter.next()); // { done: true, value: undefined } 
Copy the code

We iterate through the array sequentially by creating iterators and calling the next() method until no new values are produced. 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, only traversing the iterable independently:

let arr = ['foo'.'bar'];
let iter1 = arr[Symbol.iterator](); 
let iter2 = arr[Symbol.iterator](); 
 
console.log(iter1.next()); // { done: false, value: 'foo' } 
console.log(iter2.next()); // { done: false, value: 'foo' } 
console.log(iter2.next()); // { done: false, value: 'bar' } 
console.log(iter1.next()); // { done: false, value: 'bar' } 
Copy the code

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 reflects the change:

let arr = ['foo'.'baz']; 
let iter = arr[Symbol.iterator](); 
 
console.log(iter.next()); // { done: false, value: 'foo' } 
 
// Insert values in the middle of the 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: true, value: undefined } 
Copy the code

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 following example compares an explicit iterator implementation with a native iterator implementation.

// This class implements Iterable interfaces.
// Calling the default iterator factory function returns
// An Iterator object that implements an Iterator interface
class Foo {[Symbol.iterator]() {    
   return {     
     next() {     
       return { done: false.value: 'foo'}; }}}}let f = new Foo(); 
 
// Prints an object that implements the iterator interface
console.log(f[Symbol.iterator]()); // { next: f() {} } 
 
// The Array type implements an Iterable interface
// Call the default iterator factory function of type Array
// An instance of ArrayIterator is created
let a = new Array(a);// Prints an instance of ArrayIterator
console.log(a[Symbol.iterator]()); // Array Iterator {} 
Copy the code

7.2.3 Customizing Iterators

Like the Iterable interface, any object that implements the Iterator interface can be used as an Iterator. Initializing an iterator with no content can only iterate a certain number of times. 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 return the iterator via 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

Each iterator created in this way also implements the Iterable interface. The factory function referenced by the symbol. iterator property returns the same iterator:

let arr = ['foo'.'bar'.'baz'];
let iter1 = arr[Symbol.iterator](); 
 
console.log(iter1[Symbol.iterator]);  // f values() { [native code] } 
 
let iter2 = iter1[Symbol.iterator](); 
 
console.log(iter1 === iter2);         // true 
Copy the code

7.2.4 Terminate iterators early

The optional return() method is used to specify the logic to execute if the iterator is prematurely closed. A structure that performs an iteration can “close” the iterator when it wants to let the iterator 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}; }},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. 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 // Exit early
 
for (let i of iter) {   
  console.log(i); 
} / / 4 / / 5
Copy the code

7.3 the generator

Generators are an extremely flexible addition to ECMAScript 6, with the ability to pause and resume code execution within a function block. This new capability has far-reaching implications, such as the ability to customize iterators and implement coroutines using generators.

7.3.1 Generator basics

The form of a generator is a function whose name is preceded by an asterisk (*) to indicate that it is a generator. A generator can be defined wherever a function can be defined, and the asterisk identifying the generator function is not affected by space on either side:

// Generator function declaration
function* generatorFn() {} 
 
// Generator function expression
let generatorFn = function* () {} 
 
// a generator function that acts as an object literal method
let foo = {    * generatorFn(){}}// a generator function that acts as a class instance method
class Foo {*generatorFn(){}}// a generator function as a static method of the class
class Bar {   static * generatorFn(){}}// Equivalent generator functions:
function* generatorFnA() {}
function *generatorFnB() {} 
function * generatorFnC() {} 
 
// Equivalent generator methods:
class Foo {*generatorFnD() {}  
  * generatorFnE(){}}Copy the code

Arrow functions cannot be used to define generator functions.

Calling a generator function produces a generator object. The generator object starts out in a suspended state. Like iterators, generator objects implement the Iterator interface and therefore have the next() method. Calling this method causes the generator to start or resume execution. The return value of the next() method is like an iterator, with a done attribute and a value attribute. Generator functions with an empty body do not stop in the middle; a call to next() will bring the generator to the done: true state.

function* generatorFn() {} 
 
let generatorObject = generatorFn(); 
 
console.log(generatorObject);         // generatorFn {<suspended>}
console.log(generatorObject.next());  // { done: true, value: undefined } 
Copy the code

The value attribute is the return value of the generator function. The default value is undefined, which can be specified by the return value of the generator function:

function* generatorFn() {   return 'foo';  } 
 
let generatorObject = generatorFn(); 
 
console.log(generatorObject);         // generatorFn {<suspended>}
console.log(generatorObject.next());  // { done: true, value: 'foo' } 
Copy the code

The generator function will only start executing after the first call to the next() method, as shown below:

function* generatorFn() {    console.log('foobar'); } 
 
// The first call to the generator function does not print a log
let generatorObject = generatorFn(); 
 
generatorObject.next();  // foobar 
Copy the code

Generator objects implement the Iterable interface, and their default iterators are self-referential:

function* generatorFn() {} 
 
console.log(generatorFn); // f* generatorFn() {} 
console.log(generatorFn()[Symbol.iterator]); // f [Symbol.iterator]() {native code}
console.log(generatorFn()); // generatorFn {<suspended>} 
console.log(generatorFn()[Symbol.iterator]()); // generatorFn {<suspended>} 
 
const g = generatorFn();  
 
console.log(g === g[Symbol.iterator]()); // true 
Copy the code

7.3.2 Interrupt execution by yield

The yield keyword allows the generator to stop and start execution, and is where the generator is useful. The generator function executes normally until the yield keyword is encountered. When this keyword is encountered, execution stops and the state of the function scope is preserved. A stopped generator function can only resume execution by calling the next() method on the generator object:

function* generatorFn() {   yield;  } 
 
let generatorObject = generatorFn(); 
 
console.log(generatorObject.next());  // { done: false, value: undefined } 
console.log(generatorObject.next());  // { done: true, value: undefined } 
Copy the code

The yield keyword is a bit like the intermediate return statement of a function, producing the value that will appear in the object returned by the next() method. A generator function that exits with the yield keyword is in the done: false state; Generator functions that exit with the return keyword are in the done: true state.

function* generatorFn() {  
  yield 'foo'; 
  yield 'bar';  
  return 'baz'; 
} 
 
let generatorObject = generatorFn(); 
 
console.log(generatorObject.next());  // { done: false, value: 'foo' } 
console.log(generatorObject.next());  // { done: false, value: 'bar' }
console.log(generatorObject.next());  // { done: true, value: 'baz' } 
Copy the code

The execution flow within a generator function is scoped for each generator object. Calling next() on one generator object does not affect other generators:

function* generatorFn() {   
  yield 'foo';   
  yield 'bar';  
  return 'baz'; 
} 
 
let generatorObject1 = generatorFn(); 
let generatorObject2 = generatorFn(); 
 
 
console.log(generatorObject1.next()); // { done: false, value: 'foo' } 
console.log(generatorObject2.next()); // { done: false, value: 'foo' } 
console.log(generatorObject2.next()); // { done: false, value: 'bar' } 
console.log(generatorObject1.next()); // { done: false, value: 'bar' }
Copy the code

The yield keyword can only be used inside a generator function and will throw an error if used elsewhere. Similar to the return keyword for functions, the yield keyword must be directly in the generator function definition, and occurrence in nested non-generator functions throws a syntax error:

/ / effective
function* validGeneratorFn() {   yield;  } 
 
/ / is invalid
function* invalidGeneratorFnA() {   function a() {     yield; }}/ / is invalid
function* invalidGeneratorFnB() {   const b = () = > {     yield; }}/ / is invalid
function* invalidGeneratorFnC() {(() = > {     yield; }) (); }Copy the code

1. Generator objects as iterables

Calling the next() method explicitly on a generator object is not very useful. In fact, generator objects are more convenient to use if they are considered iterable:

function* generatorFn() {  
  yield 1;  
  yield 2; 
  yield 3;
} 
 
for (const x of generatorFn()) {  
  console.log(x); 
} // 1/2/3
Copy the code

2. Use yield to implement input and output

In addition to being used as an intermediate return statement of a function, the yield keyword can also be used as an intermediate argument to a function. The yield keyword that paused the generator function the last time receives the first value passed to the next() method. Here’s a bit of a puzzle — the value passed in the first call to next() will not be used because the call is to start the generator function:

function* generatorFn(initial) {   
  console.log(initial);    
  console.log(yield);   
  console.log(yield);
} 
 
let generatorObject = generatorFn('foo'); 
 
generatorObject.next('bar');  // foo
generatorObject.next('baz');  // baz 
generatorObject.next('qux');  // qux 

// The yield keyword can be used for both input and output, as shown in the following example:
function* generatorFn() {    
  return yield 'foo'; 
} 
 
let generatorObject = generatorFn(); 
 
console.log(generatorObject.next());       // { done: false, value: 'foo' }
console.log(generatorObject.next('bar'));  // { done: true, value: 'bar' } 
Copy the code

Because the function must evaluate the entire expression to determine the value to return, it pauses execution when it hits the yield keyword and evaluates the value to produce: “foo”. The next call to next() passed in “bar” as the value given to the same yield. This value is then determined to be the value to be returned by the generator function. The yield keyword is not used only once. For example, the following code defines an infinite count generator function:

function* generatorFn() {   
  for (let i = 0;; ++i) {yieldi; }}let generatorObject = generatorFn(); 
 
console.log(generatorObject.next().value);  / / 0
console.log(generatorObject.next().value);  / / 1
console.log(generatorObject.next().value);  / / 2
console.log(generatorObject.next().value);  / / 3
console.log(generatorObject.next().value);  / / 4
console.log(generatorObject.next().value);  / / 5.Copy the code

3. Generate iterables

You can use an asterisk to enhance the behavior of yield so that it iterates over an iterable, thus producing one value at a time:

// Equivalent generatorFn:
// function* generatorFn() { 
// for (const x of [1, 2, 3]) {
// yield x; / /}
// } 
function* generatorFn() {   
  yield* [1.2.3]; 
} 
 
let generatorObject = generatorFn(); 
 
for (const x of generatorFn()) {   console.log(x); } // 1/2/3
Copy the code

Because yield* is really just a serialization of an iterable into a series of values that can be produced individually, this is no different than putting yield into a loop. The behavior of the following two generator functions is equivalent:

function* generatorFnA() { 
  for (const x of [1.2.3]) { 
    yieldx; }}for (const x of generatorFnA()) {   
  console.log(x); 
} // 1/2/3
 
function* generatorFnB() { 
  yield* [1.2.3];
} 
 
for (const x of generatorFnB()) {
  console.log(x); 
} // 1/2/3
Copy the code

4. Use yield* to implement recursive algorithms

Where yield* is useful is to implement recursive operations, where the generator can produce itself. Look at the following example:

function* nTimes(n) {   
  if (n > 0) {  
    yield* nTimes(n - 1);  
    yield n - 1; }}for (const x of nTimes(3)) {  
  console.log(x); 
} // 1/2
Copy the code

7.3.3 Generators as default iterators

Because generator objects implement the Iterable interface, and because both generator functions and default iterators are called to produce iterators, generators are particularly suitable as default iterators. Here is a simple example of a class whose default iterator produces the contents of the class in a single line of code:

class Foo {    
  constructor() {   
    this.values = [1.2.3];   
  } 
  * [Symbol.iterator]() {   
    yield* this.values; }}const f = new Foo(); 
for (const x of f) {
  console.log(x);
} // 1/2/3
Copy the code

7.3.4 Premature termination of the generator

Like iterators, generators support the concept of “closeable.” An object that implements the Iterator interface must have a next() method and an optional return() method to prematurely terminate the Iterator. In addition to these two methods, generator objects have a third method: throw(). Both the return() and throw() methods can be used to force the generator into a closed state.

function* generatorFn() {} 
 
const g = generatorFn(); 
 
console.log(g);         // generatorFn {<suspended>}
console.log(g.next);    // f next() { [native code] }
console.log(g.return);  // f return() { [native code] }
console.log(g.throw);   // f throw() { [native code] } 
Copy the code

1. return()

The return() method forces the generator into a shutdown state. The value supplied to the return() method is the terminating iterator object:

function* generatorFn() {    
  for (const x of [1.2.3]) {    
    yieldx; }}const g = generatorFn(); 
 
console.log(g);            // generatorFn {<suspended>} 
console.log(g.return(4));  // { done: true, value: 4 } 
console.log(g);            // generatorFn {<closed>} 
Copy the code

Unlike iterators, all generator objects have a return() method that, once entered into a closed state, cannot be recovered. A subsequent call to next() displays the done: true state, and any return value provided is not stored or propagated:

function* generatorFn() {    
  for (const x of [1.2.3]) {    
    yieldx; }}const g = generatorFn(); 
 
console.log(g.next());     // { done: false, value: 1 } 
console.log(g.return(4));  // { done: true, value: 4 } 
console.log(g.next());     // { done: true, value: undefined } 
console.log(g.next());     // { done: true, value: undefined } 
console.log(g.next());     // { done: true, value: undefined } 
Copy the code

2. throw()

The throw() method injects a supplied error into the generator object when paused. If the error is not handled, the generator closes:

function* generatorFn() {
  for (const x of [1.2.3]) {   
    yieldx; }}const g = generatorFn(); 
 
console.log(g);   // generatorFn {<suspended>} 
try {   
  g.throw('foo'); 
} catch (e) {  
  console.log(e); // foo 
}
console.log(g);   // generatorFn {<closed>}
Copy the code

However, if the error is handled internally by the generator function, the generator is not shut down and execution can resume. Error handling skips the corresponding yield, so in this case a value is skipped. Such as:

function* generatorFn() {  
  for (const x of [1.2.3]) {  
    try {      
      yield x; 
    } catch(e) {}  
  }
} 
const g = generatorFn(); 
 
console.log(g.next()); // { done: false, value: 1} 
g.throw('foo');
console.log(g.next()); // { done: false, value: 3} 
 
Copy the code

If the generator object has not yet started executing, the error thrown by the call to throw() will not be caught inside the function, because it is equivalent to throwing an error outside the function block.

7.4 summary

Iteration is a pattern seen in all programming languages. ECMAScript 6 officially supports iterative mode and introduces two new language features: iterators and generators.

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 native consumers such as for-of loops.

A generator is a special function that returns a generator object when called. Generator objects implement the Iterable interface and can therefore be used anywhere an Iterable is consumed. Generators are unique in that they support the yield keyword, which suspends execution of a generator function. The yield keyword is also used to receive input and produce output through the next() method. With an asterisk, the yield keyword serializes the iterable that follows it into a series of values.

References:

JavaScript Advanced Programming (Version 4)