The preface

This section is short, but many of the key definitions are important enough to be understood more concretely by breaking them down and verifying them with examples. So let’s start with the core of this chapter:

Iterators and generators are new features in ES6 designed to make iterating easier and cleaner.

MIND

The iteration

What is iteration

Iteration is the execution of a program several times in sequence, usually with explicit termination conditions.

Extract key words: “order”, “repeated many times”, “termination condition”, and analyze through examples:

const arr = [1.2.3.4];
for (let i = 0, len = arr.length; i < len; i++) {
  console.log(i);
}
/ / 1
/ / 2
/ / 3
/ / 4
Copy the code

The array loop above is a simple iteration, and loops are the basis of the iteration mechanism to check for three key words:

  • Order: Iteration occurs over an ordered set, where “ordered” means that all items in the set can be traversed in a given order, especially if the beginning and end items are clearly defined. The array ARR is the most typical example of an ordered set.
  • Repeat multiple times: Each loop is completed before the next iteration begins. (Execute statementconsole.log(i)Elements that output arR in turn)
  • Termination condition: The loop specifies the number of iterations. Loop condition statement fori < lenDefine loop times)

The pitfalls of array iteration

  • You need to know how to use data structures before you iterate. Data structures must be met through[]Takes an item at a specific index position
  • The traversal order is not inherent in the data structure. Does not apply to data structures with implicit order other than divisor groups

ES5 iterative

To remedy this shortcoming, ES5 has added some iterative methods, which were introduced in the previous chapter on collection reference types

  • forEach()
  • map()
  • some()
  • every()
  • filter()

The above method does not need to fetch the item at a specific index position through [], but it is still the method on the array prototype and does not apply to other data structures

The iterator

The new iterator pattern in ES6 allows development languages such as Python and Java to solve iteration defects through native language constructs, allowing developers to iterate without having to know how to iterate in advance.

The Iterator pattern describes a scenario in which structures that implement the formal Iterable interface and can be consumed by iterators are called “iterables.”

Well, this explanation is not easy to understand, so break it down:

The Iterable protocol and iterator protocol are added in ES6. The objects that implement Iterable protocol (Iterable interface) are called iterables, and the objects that implement iterator protocol are called iterators. The iterables are “consumed” by iterators during iteration.

Let’s start with the agreement.

Iterable protocol

Iterable protocols have two capabilities:

  • Support iterative self-identification
  • The ability to create objects that implement an iterable protocol

First, why these two abilities:

  • First, self-identification, which requires prior knowledge of the defects of the iterator before solving ES6.
  • The second point, creating objects, addresses the pre-ES6 defect of limiting iteration to arrays.

In fact, these two capabilities can be summarized as the ability to define or customize the iterative behavior of JS objects. Let’s see how this is implemented:

Iterable complies with the iterable protocol, which defines the behavior of iteration by exposing the default iterator property in an object on the object or prototype chain. This property takes the Symbol. Iterator constant as the key and refers to an iterator factory function to return a new iterator.

Iterator, also known as @@iterator, is a common built-in Symbol used to expose behavior within a language and to directly access, rewrite, and simulate that behavior. These built-in symbols all exist as string properties of the Symbol factory function. Therefore, it is also possible to redefine the value of symbol. iterator in a custom object to implement custom iterative behavior.

iterable

With the iterable protocol in mind, let’s see which objects are iterable:

  • Built-in iterables
    • String
    • Array
    • TypedArray
    • Map
    • Set
    • The arguments object for the function
    • DOM collection type such as NodeList
  • You need the syntax of an iterable
    • for-of
    • Deconstruction assignment
    • Extended operator
    • yield*
  • A built-in API that receives iterables
    • new Map([iterable])
    • new WeakMap([iterable])
    • new Set([iterable])
    • new WeakSet([iterable])
    • Promise.all(iterable)
    • Promise.race(iterable)
    • Array.from(iterable)
  • Custom iterable
Finally, MDN mentions that there are poorly formed iterables, but exceptions occur because its iteration factory function cannot undo iterator objects.Copy the code

Since an iterable implements an iterable protocol, let’s take a look

  • Built-in iterables
let str = 'JayeZhu';
// Output the default iterator property and return the iterator factory function
console.log(str[Symbol.iterator]); // f values() { [native code] }
// Call the iterator factory function to generate a new iterator
console.log(str[Symbol.iterator]()); // StringIterator {}
Copy the code
  • Native language constructs (requires iterable syntax and built-in apis to receive iterables)
let arr = [1.2.3]
// Create iterators by calling the provided iterable factory function behind the scenes
console.log([...arr]); / / [1, 2, 3]
Copy the code

To customize an iterable, you need to understand the iterator protocol and iterators and see what the operations are.

Iterator protocol

The iterator protocol defines a standard way to produce a set of values, whether finite or wireless. When a finite number of values are iterated over, a default return value is returned.

This standard approach requires iterators that satisfy the iterator protocol, and only objects that implement the next() method can be iterators.

Next () is a parameterless function that returns an IteratorReault object with done and value attributes:

  • Done, Boolean, false means you can also call next() to get the next value, true means “exhausted”
  • Value, if done is true, contains the next value of the iterable, otherwise undefined

The specific scenario is as follows

let arr = [1.2];
let iter = arr[Symbol.iterator](); // Get the iterator
// Execute the iterator
console.log(iter.next()); // {value: 1, done: false}
console.log(iter.next()); // {value: 1, done: false}
console.log(iter.next()); // {value: undefined, done: true}
console.log(iter.next()); // {value: undefined, done: true}
Copy the code

Note that iterables can be iterated over sequentially through the iterator’s next() method, returning IteratorReault. When the done attribute of IteratorReault is true, the same IteratorReault object is returned.

Therefore, this explains that when values are finite, the default value is returned when all values are iterated over, whereas when wireless, the done value is always false, and the iteration continues.

The iterator

Now that you know the iterator protocol, it’s time for iterators to show up. An iterator is an object that defines a sequence and may return a return value on termination. Has the following characteristics:

  • Disposable objects: Each call to the iterator factory function generates a new iterator object
  • Associating an iterable: Prevents the garbage collector from reclaiming the iterable

So, let’s see what a simple iterator looks like:

class Color {
  / / the iterator
  [Symbol.iterator] () {
    return {
    	/ / next () function
      next () {
      	/ / return IteratorReault
        return { done: false.value: 'cyan'}; }}}}const color = new Color();
console.log(color[Symbol.iterator]()); // { next: f () {} }
console.log(color[Symbol.iterator]().next()); // {done: false, value: "cyan"}
Copy the code

Where class represents a class, which will appear in the next chapter

Custom iterators

To be able to iterate over an iterable more than once, we need to create pairs of iterators on the iterator, each corresponding to a new counter, and put the counter variable into a closure that returns the iterator.

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

  [Symbol.iterator] () {
    let count = 1,
      limit = this.limit;
    retrun  {
      next () {
        if (count <= limit) {
          return { done: false.value: count++ };
        } else {
          return { done: true.value: undefined };
        }
      }
    }
  }
}
Copy the code

Terminate the iterator early

Iterators use the optional return() method to close execution early. Application scenarios include:

  • The for-of loop exits prematurely with break\continue\return\throw, etc
  • Deconstructing assignment does not consume all values

Put custom iterators into the return() method

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}; }},/ / to join the return
      return () {
      	console.log('Operation before iterator closes');
        return { done: true}; }}}}let counter = new Counter (5);

for (let i of counter) {
  if (i > 2) break;
  console.log(i);
}
/ / 1
/ / 2
// Perform the operation before the iterator closes
Copy the code

Note:

  • If return() is not set to close the iterator, or if the iterator cannot be closed, the iteration exits and continues where it left off
  • Iterators with return() are not forced to close, but return() is called

The generator

A generator is a structure that can pause and resume execution within a function.

It takes the form of a function (explained in Chapter 10) and is preceded by an asterisk (*). In addition to the arrow function, other functions can define generator functions.

Generator basics

  • A statement with*, the space between the asterisk does not affect
  • Calling a generator function produces a generator object
  • The generator object is initially suspended
  • Generator objects have a next() method similar to iterators
  • The generator function will only start execution after the first call to the next() method
  • Generator functions implement the iterable protocol, and the default iterator is self-referential

Write one on the top

let generatorFn = function* () {
  console.log('execution');
  return 'generator'
};

const g  = generatorFn();
console.log(g); // generatiorFn {<suspended>}
console.log(g.next);ƒ next() {[native code]}
console.log(g.next());
/ / execution
// {value: "generator", done: true}

console.log(generatorFn);
/* ƒ* () {console.log(' run '); return 'generator' }*/
console.log(generatorFn()[Symbol.iterator]); ƒ [Symbol. Iterator]() {[native code]}
console.log(generatorFn()); // generatorFn {<suspended>}
console.log(generatorFn()[Symbol.iterator]()); // generatorFn {<suspended>}
Copy the code

yield

Recall the concept of generators: structures that can be paused and resumed inside functions. This internal pause and reply is done with the yield keyword, so yield is the most useful part of a generator.

The properties of yield are:

  • Yield can only be used inside the generator; errors are reported elsewhere
  • The generator function executes normally until it hits yield
  • When encountered, execution stops and the function scope is preserved
  • The generator object is called next() to resume the generator execution that yield has paused
  • Yield returns the generated value to the object returned by next()
  • The generator exits and is in the done: true state until a return is encountered
function* generatorFn() {
  console.log('generator');
  yield '1';
  yield '2';
  return 'return';
}
const g = generatorFn();

console.log(g.next());
// generator
// {value: "1", done: false}
console.log(g.next()); // {value: "2", done: false}
console.log(g.next()); // {value: "return", done: true}
Copy the code

So what does this mechanism do?

Generator object as iterator object

It is much easier to generate custom iterators using generator objects, avoiding the need for less-recommended closures because of control loops

function* nTimes (n) {
  while (n--) {
    yield; }}for (let time of nTimes(3)) {
  console.log(`time:${time}`);
}
// time:undefined
// time:undefined
// time:undefined
Copy the code

Yield implements input and output

The last yield receives the first value of the current next() method, but the first call to next() only starts the generator function, so the first value of the first next() will not be used by yield.

function* generatorFn (value) { console.log(value); console.log(yield); console.log(yield); } const g = generatorFn(100); g.next(1); // 100 => value, the first time not to receive g.ext (2); // 2 g.next(3); / / 3Copy the code

In this way, the above iterator object is modified to implement input and output

function* nTimes (n) {
  let i = 0;
  while(n--) { yeild i++; }}for (let time of nTimes(3)) {
  console.log(`time:${time}`);
}
// time:0
// time:1
// time:2
Copy the code

Produces an iterable

Yield can also be used with *, which is used to enhance yield by iterating over an iterable to produce a value.

Therefore, the above example can be optimized

function* nTimes (n) {
  yield* Array.from({ length: n }, (x, i) = > i);
}
for (let time of nTimes(3)) {
  console.log(`time:${time}`);
}
// time:0
// time:1
// time:2
Copy the code

Yield * implements recursion

This is where yield* is most useful, and the reason it makes recursion possible is that the generator can generate itself

Then the above example is reformed

function* nTimes (n) {
  if (n > 0) {
    yield* nTimes(n - 1);
    yield n -1; }}for (let time of nTimes(3)) {
  console.log(`time:${time}`);
}
// time:0
// time:1
// time:2
Copy the code

The yield* nTimes(n-1) produces a new iterable each time, and the resulting iterable, which is itself, continues iterating and yields a yield* integer. This is equivalent to creating an iterable and returning an incrementing integer

Generator as default iterator

Because yield* produces an iterable, it implements an iterable protocol and is suitable as a default iterator.

The above custom iterator can therefore be modified to a cleaner generator version

class Counter {
  constructor (limit) {
    this.values = Array.from({ length: n }, (x, i) = > i);
  }

  * [Symbol.iterator] () {
    yield* this.values; }}}const counter = new Counter(3);
Copy the code

Premature termination generator

Since generators can be used as default iterators, they must also support early closing. In addition to return() as a termination method, generators also support throw() to force the generator into a shutdown state.

It is indicated in ES6 series that iterator also has throw method, but it is used with GeneratorCopy the code

return()

The return() methods provided by generators are quite different from those provided by iterators:

  • Optional: Generators are availablereturn()Method, and iterators are optional
  • Mandatory: of a generatorreturn()Methods force a closed state, while iterators do not force it, but the method executes
  • Pass value: generator’sreturn()Methods can beThe value of, and the value is the value of the terminating iterator object, which must return oneobject
function* generatorFn() {
  for (const x of [1.2.3]) {
    yieldx; }}const g1 = generatorFn();
console.log(g1); // generatorFn {<suspended>}
console.log(g.return(4)); // { done: true, value: 4 }
console.log(g); // generatorFn {<closed>}

// The for-of loop ignores the value returned inside the done: true iterator
const g2 = generatorFn();
for (const x of g2) {
  if (x > 1) g.return(4);
  console.log(x);
}
/ / 1
/ / 2
Copy the code

throw()

This method injects a supplied error into the generator object.

If the error is not handled, the generator is shut down.

function* generatorFn () {
  for (const x of [1.2.3]) {
  yield x;
}

const g = generatorFn();
console.log(g); // generatorFn {<suspended>}
try {
  g.throw('err');
} catch (e) {
  console.log(e); // "err"
}
console.log(g); // generatorFn {<closed>}
Copy the code

If the generator function handles this error internally, the generator will not be shut down, but will resume execution.

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('err');
console.log(g.next()); // { done: false, value: 2 }
Copy the code

Error handling skips the corresponding yield, so 2 is skipped, and execution resumes later.

conclusion

The iterators and generators added to ES6 do a good job of solving the pre-ES6 iteration problem.

Iterables that implement iterable protocols are “consumed” during iteration by iterators that implement iterator protocols.

Iterables have the default iterator property symbol. iterator, which refers to the iterator factory function that returns the iterator object.

Iterators get consecutive values by calling next() continuously and return IteratorrObject with done and value attributes.

The iterator performs the terminating action via the optional return(), but does not necessarily terminate the iteration.

Generators are special functions that return a generator object when called. This object implements the iterable protocol and can be used as the default iterator.

Generator objects support yield to suspend generator functions, and they can do a lot of input, output, and recursion.

All generator objects have the return() method to close the iterator and return the value. There is also the throw() method to handle exceptions.

The articles

  • 1- What is JS
  • 2 – HTML in JavaScript
  • 3- Language basics
  • 4- Variables, scope, and memory
  • 5- Basic reference types
  • 6- Collection reference type

If you think the article is good or helpful, I hope to get a like (bang bang bang), and if you think there is a problem, I also hope to make suggestions for improvement, thank you!