Programmer Yuanxiao worked overtime to do code: generator

Regular functions return only one value (or none).

A generator can return (” yield “) multiple values, one after the other, as needed. They work well with iterables, allowing for easy creation of data flows.

Generator function

To create a generator, we need a special syntactic construct :function*, known as a “generator function.”

It goes like this:

function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}
Copy the code

Generator functions behave differently from regular functions. When such a function is called, it does not run its code. Instead, it returns a special object, called a “generator object,” to manage execution.

Check it out:

function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}

// "generator function" creates "generator object"
let generator = generateSequence();
alert(generator); // [object Generator]
Copy the code

The function code has not yet started executing:

Copy the code

The main method for generators is next(). When called, it is executed until the latest yield statement (value can be omitted, so it is undefined). The function then pauses and the generated value is returned to the external code.

The result of next() is always an object with two attributes:

  • value: Generated value.
  • done: if the function code has already been completedtrue, or forfalse.

For example, here we create a generator and get its first generated value:

function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}

let generator = generateSequence();

let one = generator.next();

alert(JSON.stringify(one)); // {value: 1, done: false}
Copy the code

So far, we only get the first value, and the function executes on the second line:

Copy the code

Let’s call generator.next() ‘again. It continues executing the code and returns the next yield ‘:

let two = generator.next();

alert(JSON.stringify(two)); // {value: 2, done: false}
Copy the code

And, if we call it a third time, the execution will reach the return statement that terminates the function:

Copy the code

Now the generator is complete. We should see done:true and Process Value :3 as the final result.

A new call to generator.next() no longer makes sense. If we execute them, they return the same object :{done: true}.

The function * f (…). The or function * f (…). ?

Both grammars are correct.

But the first syntax is usually preferred because the asterisk * indicates that it is a generator function that describes a type, not a name, so it should stick with the function keyword.

Generators are iterable

When you looked at the next() method, you might have guessed that generators are iterable.

We can use for.. The of loop iterates through their values:

function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}

let generator = generateSequence();

for(let value of generator) {
  alert(value); // 1, then 2
}
Copy the code

Looks a lot better than calling.next().value, right? But note: the example above shows 1, then 2, and that’s it. It doesn’t show 3!

This is because the last value :true is ignored when executing on.. Therefore, if we want for.. Of displays all the results. For, we must back down:

function* generateSequence() {
  yield 1;
  yield 2;
  yield 3;
}

let generator = generateSequence();

for(let value of generator) {
  alert(value); // 1, then 2, then 3
}
Copy the code

Since generators are iterable, we can invoke all related functions, such as the spread syntax… :

function* generateSequence() {
  yield 1;
  yield 2;
  yield 3;
}

let sequence = [0. generateSequence()]; alert(sequence);// 0, 1, 2, 3
Copy the code

In the code above… GenerateSequence () converts an iterable generator object into a set of items (see spread syntax in the Rest Parameters and Spread Syntax chapter).

Using generators for iterables

let range = {
  from: 1.to: 5.// for.. of range calls this method once in the very beginning
  [Symbol.iterator]() {
    / /... it returns the iterator object:
    // onward, for.. of works only with that object, asking it for next values
    return {
      current: this.from,
      last: this.to,

      // next() is called on each iteration by the for.. of loop
      next() {
        // it should return the value as an object {done:.. , value :... }
        if (this.current <= this.last) {
          return { done: false.value: this.current++ };
        } else {
          return { done: true}; }}}; }};// iteration over range returns numbers from range.from to range.to
alert([...range]); / / 1, 2, 3, 4, 5
Copy the code

You can use the generator function to iterate by providing it as symbol. iterator.

Here’s the same range, but more compact:

let range = {
  from: 1.to: 5, * [Symbol.iterator]() { // a shorthand for [Symbol.iterator]: function*()
    for(let value = this.from; value <= this.to; value++) {
      yieldvalue; }}}; alert( [...range] );/ / 1, 2, 3, 4, 5
Copy the code

This works because range[symbol.iterator]() now returns a generator, and the generator method is for.. Of expected:

  • There are.next()methods
  • return{value: ... , done: true/false}

Of course, this is no coincidence. Generators were added to the JavaScript language with iterators in mind to make them easy to implement.

The variant with the generator is much cleaner than Range’s original iterable code and retains the same functionality.

Generators can always generate values

In the example above, we generated a finite sequence, but we could also generate a generator that permanently generates values. For example, an infinite sequence of pseudo-random numbers.

Generator combination

Generator composition is a special feature of generators that allows generators to be transparently “embedded” into each other.

For example, we have a function that generates a sequence of numbers:

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) yield i;
}
Copy the code

Now we want to reuse it to generate a more complex sequence:

  • First, the number 0… 9(character code 48… 57),
  • Followed by A capital letter A.. Z(character code 65… 90).
  • Followed by the lowercase letter A.. Z (character code 97… 122).

We could use this sequence, for example, to create a password by selecting the characters in it (we could also add syntactic characters), but let’s leave it as it is.

In a regular function, to combine results from multiple other functions, we call them, store the results, and then concatenate at the end.

For generators, there is a special yield* syntax for “embedding” (combining) one generator into another.

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) yield i;
}

function* generatePasswordCodes() {

  / / 0.. 9
  yield* generateSequence(48.57);

  // A.. Z
  yield* generateSequence(65.90);

  // a.. z
  yield* generateSequence(97.122);

}

let str = ' ';

for(let code of generatePasswordCodes()) {
  str += String.fromCharCode(code);
}

alert(str); / / 0.. 9A.. Za.. z
Copy the code

The yield* instruction delegates execution to another generator. This term means that yield* gen iterates over the generator generator and transparently passes its yield externally. It is as if the values were generated by an external generator.

The result is the same as inlining code from a nested generator:

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) yield i;
}

function* generateAlphaNum() {

  // yield* generateSequence(48, 57);
  for (let i = 48; i <= 57; i++) yield i;

  // yield* generateSequence(65, 90);
  for (let i = 65; i <= 90; i++) yield i;

  // yield* generateSequence(97, 122);
  for (let i = 97; i <= 122; i++) yield i;

}

let str = ' ';

for(let code of generateAlphaNum()) {
  str += String.fromCharCode(code);
}

alert(str); / / 0.. 9A.. Za.. z
Copy the code

Generator composition is a natural way to plug one generator stream into another. It does not use additional memory to store intermediate results.

yieldIt’s a two-way street

Previously, generators were similar to Iterable objects, using special syntax to generate values. But in fact, they are more powerful and flexible.

This is because yield is a two-way channel: it not only returns results externally, it can also pass values inside the generator.

To do this, we should call generator.next(arg) with one argument. This argument is the result of yield.

Let’s look at an example:

function* gen() {
  // Pass a question to the outer code and wait for an answer
  let result = yield "2 plus 2 equals?"; / / (*)

  alert(result);
}

let generator = gen();

let question = generator.next().value; // <-- yield returns the value

generator.next(4); // --> pass the result into the generator
Copy the code

The first call to generator.next() should always take no arguments (if passed, the arguments are ignored). It starts execution and returns the first yield, “2+2=?” . The generator pauses at this point and remains on the (*) line.

Then, as shown in the figure above, the yield goes into the question variable in the calling code.

On generator.next(4), the generator resumes with a result of 4:let result = 4.

Note that external code does not have to call next(4) immediately. That may take time. That’s not a problem: generators wait.

Such as:

// resume the generator after some time
setTimeout(() = > generator.next(4), 1000);
Copy the code

As we can see, unlike regular functions, the generator and calling code can exchange results by passing values in next/yield.

To make things more obvious, here’s another example with more calls:

function* gen() {
  let ask1 = yield "2 plus 2 equals?";

  alert(ask1); / / 4

  let ask2 = yield "3 times 3 is what?"

  alert(ask2); / / 9
}

let generator = gen();

alert( generator.next().value ); // "2 + 2 =?"

alert( generator.next(4).value ); // "3 * 3 =?"

alert( generator.next(9).done ); // true
Copy the code

Implementation:

  • First. Next () starts executing… It reaches the first yield.

The result is returned to the external code.

  • Next (4) passes 4 back to the generator as the result of the first yield and continues execution.

  • It achieves the second benefit, which becomes the result of a generator call.

  • The third next(9) passes 9 to the generator as the result of the second yield and continues to perform the operation that reaches the end of the function, completing :true.

It’s like a game of “ping pong”. Each next(value)(excluding the first) passes a value to the generator, which becomes the result of the current yield, which then returns the result of the next yield.

generator.throw

As we observed in the example above, external code can pass a value to the generator as the result of yield.

But it can also throw an error there. This is natural, because mistakes are consequences.

To pass an error to yield, you should call generator.throw(err). In this case, an ERR is thrown on the row that has that yield.

For example, here “2 + 2 =?” The yield of the

function* gen() {
  try {
    let result = yield "2 plus 2 equals?"; / / (1)

    alert("The execution does not reach here, because the exception is thrown above");
  } catch(e) {
    alert(e); // shows the error}}let generator = gen();

let question = generator.next().value;

generator.throw(new Error("The answer is not found in my database")); / / (2)
Copy the code

The 382/5000 error thrown into the generator at line (2) causes a yield exception on line (1). In the example above, try.. Catch it and show it.

If we don’t catch it, then like any exception, it will “fall” from the generator into the calling code.

The calling code is preceded by a generator. Throw, labeled (2). So we can catch it here, like this:

function* generate() {
  let result = yield "2 plus 2 equals?"; // Error in this line
}

let generator = generate();

let question = generator.next().value;

try {
  generator.throw(new Error("The answer is not found in my database"));
} catch(e) {
  alert(e); // shows the error
}
Copy the code

If we don’t catch an error there, then it falls outside the calling code as usual (if any), and if not, the script is terminated.

conclusion

  • Generators are created by the generator function function* f(…). {… } create.

  • Generators have (only) internal yield operators.

  • External code and generators can exchange results through next/yield calls.

Generators are rarely used in modern JavaScript. But sometimes they come in handy, because a function’s ability to exchange data with the calling code during execution is quite unique. Of course, they are very useful for making iterable objects.

Also, in the next chapter, we’ll learn about asynchronous generators, which are used to read asynchronously generated data streams (such as paging reads over a network). The loop.

In network programming, we often use streaming data, so this is another very important use case.