[ES6 knowledge] Generator function and yield keyword

Article directory

    • 1 Generator function
      • 1.1 Overview
      • 1.2 Execution mechanism
    • 1.3 yield expression
      • 1.3.1 Overview
      • 1.3.2 Similarities and differences between `yield` expression and `return` statement
      • 1.4 next() method parameters

1 Generator function

1.1 Overview

ES6 newly introduces the Generator function, which can suspend the execution flow of the function through the yield keyword, providing the possibility to change the execution flow, thus providing a solution for asynchronous programming.

Collection of native JavaScipt cases
JavaScript + DOM basics
JavaScript basic to advanced
Canvas game development

Formally, the Generator function is an ordinary function, but it has two characteristics.

  1. There is an asterisk between the function keyword and the function name
  2. Yield expressions are used inside the function body to define different internal states (yield` means “output” in English).
function* func(){
 console.log("one");
 yield '1';
 console.log("two");
 yield '2';
 console.log("three");
 return '3';
}

Executing the Generator function will return a traverser object. That is to say, the Generator function is not only a state machine, but also a traverser object generation function. The returned traverser object can traverse each state inside the Generator function in sequence.

There are two yield expressions (1 and 2) inside the above Generator function, that is, the function has three states: 1, 2 and return statement (end execution).

1.2 Execution mechanism

Calling the Generator function is the same as calling an ordinary function. Just add () after the function name. However, the Generator function will not be executed immediately like an ordinary function. Instead, it will return a pointer to the internal state object, so the iterator object Iterator must be called. Next method, the pointer will start execution from the head of the function or where it stopped last time.

f.next();
// one
// {value: "1", done: false}
 
f.next();
// two
// {value: "2", done: false}
 
f.next();
// three
// {value: "3", done: true}
 
f.next();
// {value: undefined, done: true}

When the next method is called for the first time, execution starts from the head of the Generator function. First, one is printed, and execution stops when yield is reached, and the value ‘1’ of the expression after yield is used as the value attribute value of the returned object. This is When the function has not finished executing, the value of the done attribute of the returned object is false.
When calling next method for the second time, the same as above.
When the next method is called for the third time, three is printed first, and then the return operation of the function is performed, and the value of the expression after return is used as the value attribute value of the returned object. At this time, the function has ended, and the done attribute value is usually used. is true.
When the next method is called for the fourth time, the function has finished executing, so the returned value attribute value is undefined and the done attribute value is true. If there is no return statement when executing the third step, {value: undefined, done: true} will be returned directly.

ES6 does not specify where the asterisk between the function keyword and the function name should be written. This causes the following writing methods to pass.

function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }

Since the Generator function is still an ordinary function, the general writing method is the third way above, that is, the asterisk follows the function keyword.

1.3 yield expression

1.3.1 Overview

Since the traverser object returned by the Generator function will traverse the next internal state only by calling the next method, it actually provides a function that can suspend execution. The yield expression is the pause flag.

The operation logic of the next method of the traverser object is as follows:

(1) When encountering a yield expression, the execution of subsequent operations will be suspended, and the value of the expression immediately following yield will be used as the value of the returned object. code>valueAttribute value.

(2) The next time the next method is called, execution will continue until the next yield expression is encountered.

(3) If no new yield expression is encountered, it will run until the end of the function until the return statement, and return The value of the expression following the statement is used as the value of the value attribute of the returned object.

(4) If the function does not have a return statement, the value of the value attribute of the returned object is undefined.

It should be noted that the expression following the yield expression will only be executed when the next method is called and the internal pointer points to the statement. Therefore, it is equivalent to providing a manual method for JavaScript. The syntactic function of “Lazy Evaluation”.

function* gen() {
  yield 123 + 456;
}

In the above code, the expression 123 + 456 after yield will not be evaluated immediately. The pointer will only be moved here in the next method. The value will not be evaluated until a sentence is passed.

1.3.2 Similarities and differences between yield expression and return statement

The yield expression and the return statement have both similarities and differences. The similarity is that both return the value of the expression immediately following the statement. The difference is that every time yield is encountered, the function pauses execution and continues backward execution from that position next time, while the return statement does not have the function of position memory. In a function, you can only execute one (or one) return statement, but you can execute multiple (or multiple) yield expressions. Normal functions can only return one value, because return can only be executed once; Generator functions can return a series of values, because there can be any number of yield. From another perspective, it can also be said that Generator generates a series of values, which is where its name comes from (in English, the word generator means “generator”).

The Generator function does not need a yield expression, and then it becomes a simple suspended execution function.

function* f() {
  console.log('Executed!')
}

var generator = f();

setTimeout(function () {
  generator.next()
}, 2000);

In the above code, if the function f is an ordinary function, it will be executed when assigning a value to the variable generator. However, function f is a Generator function, so function f will only be executed when the next method is called.

1.4 next() method parameters

The yield expression itself has no return value, or it always returns undefined. The next method can take one parameter, which will be treated as the return value of the previous yield expression.

function* f() {
  for(var i = 0; true; i + + ) {
    var reset = yield i;
    if(reset) { i = -1; }
  }
}

var g = f();

g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }

The above code first defines a Generator function f that can run infinitely. If the next method has no parameters, it will run to the yield expression and variable each time. The value of reset is always undefined. When the next method takes a parameter true, the variable reset is reset to this parameter (i.e. true) , so i will be equal to -1, and the next cycle will increase from -1.

This function has very important grammatical significance. When the Generator function resumes operation from the paused state, its context remains unchanged. Through the parameters of the next method, there is a way to continue injecting values into the function body after the Generator function starts running. That is to say, different values can be injected from the outside into the inside of the Generator function at different stages to adjust the function behavior.

function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}

var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}

var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }

In the above code, the next method is run for the second time without parameters, causing the value of y to be equal to 2 * undefined (i.e. NaN). After dividing by 3, it is still NaN, so the value attribute of the returned object is also equal to NaN. The Next method is run for the third time without parameters, so z is equal to undefined, and the value attribute of the object is returned. Equal to 5 + NaN + undefined, that is, NaN.
If you provide parameters to the next method, the returned results are completely different. When the above code calls the next method of b for the first time, it returns the value 6 of x + 1; the second time Call the next method this time to set the value of the last yield expression to 12, so y is equal to 24, returns the value 8 of y / 3; the third time the next method is called, the last yield The value of the expression is set to 13, so z is equal to 13, then x is equal to 5, y is equal to 24, so the value of the return statement is equal to 42.

Note that since the parameters of the next method represent the return value of the previous yield expression, the parameters are passed when the next method is used for the first time. it is invalid. The V8 engine directly ignores the parameters when the next method is used for the first time. The parameters are only valid from the second time the next method is used. Semantically speaking, the first next method is used to start the traverser object, so it does not require parameters.