Generator function syntax

Introduction

basic concepts

Generator function is an asynchronous programming solution provided by ES6, and its syntax behavior is completely different from traditional functions. This chapter introduces the syntax and API of the Generator function in detail. For its asynchronous programming application, please refer to the chapter "Asynchronous Application of Generator Function".

The Generator function has many angles of understanding. Syntactically, it can be understood firstly that the Generator function is a state machine that encapsulates multiple internal states.

Executing the Generator function will return a iterator object, that is to say, in addition to the state machine, the Generator function is also a iterator object generation function. The returned iterator object can traverse each state in the Generator function in turn.

Formally, the Generator function is an ordinary function, but it has two characteristics. One is that there is an asterisk between the function keyword and the function name; the other is that the function body uses a yield expression to define different internal states (yield means "output" in English ).

function* helloWorldGenerator() {
  yield "hello";
  yield "world";
  return "ending";
}

var hw = helloWorldGenerator();

The above code defines a Generator function helloWorldGenerator, which contains two yield expressions (hello and world), that is, the function has three states: hello, world and return statement (end of execution).

Then, the calling method of the Generator function is the same as that of a normal function, adding a pair of parentheses after the function name. The difference is that after calling the Generator function, the function is not executed, and the return is not the result of the function, but a pointer to the internal state, which is the Iterator Object introduced in the previous chapter.

In the next step, the next method of the iterator object must be called to move the pointer to the next state. That is to say, every time the next method is called, the internal pointer is executed from the head of the function or where it stopped last time, until the next yield expression (or return statement) is encountered. In other words, the Generator function is executed in stages, the yield expression is a sign of pause execution, and the next method can resume execution.

hw.next();
// {value:'hello', done: false}

hw.next();
// {value:'world', done: false}

hw.next();
// {value:'ending', done: true}

hw.next();
// {value: undefined, done: true}

The above code calls the next method four times in total.

At the first call, the Generator function starts to execute until the first yield expression is encountered. The next method returns an object whose value attribute is the value hello of the current yield expression, and the done attribute value is false, indicating that the traversal has not ended.

In the second call, the Generator function executes from the place where the previous yield expression stopped to the next yield expression. The value attribute of the object returned by the next method is the value world of the current yield expression, and the value false of the done attribute indicates that the traversal has not yet ended.

In the third call, the Generator function is executed from where the yield expression stopped last time to the return statement (if there is no return statement, it executes until the end of the function). The value property of the object returned by the next method is the value of the expression immediately following the return statement (if there is no return statement, the value of the value property is undefined), done The value of the property true indicates that the traversal has ended.

In the fourth call, the Generator function has finished running, the value property of the returned object of the next method is undefined, and the done property is true. Calling the next method later will return this value.

To summarize, call the Generator function and return a traverser object, which represents the internal pointer of the Generator function. In the future, every time the next method of the iterator object is called, an object with two attributes value and done will be returned. The value attribute represents the value of the current internal state, which is the value of the expression following the yield expression; the done attribute is a boolean value, which indicates whether the traversal is over.

ES6 does not specify where the asterisk between the function keyword and the function name should be written. This leads to the following writing can 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 way of writing is the third above, that is, the asterisk immediately follows the function keyword. This book also uses this way of writing.

yield expression

Since the traverser object returned by the Generator function can 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 running logic of the next method of the iterator object is as follows.

(1) When encountering a yield expression, the execution of the following operations is suspended, and the value of the expression immediately following the yield is used as the value attribute value of the returned object.

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

(3) If no new yield expression is encountered again, it will run until the end of the function until the return statement, and use the value of the expression after the return statement as the value of the returned object Attribute value.

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

It should be noted that the expression behind the yield expression will only be executed when the next method is called and the internal pointer points to the statement, so it is equivalent to providing manual "lazy evaluation" for JavaScript. Grammatical function.

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

In the above code, the expression 123 + 456 after yield will not be evaluated immediately, it will only be evaluated when the next method moves the pointer to this sentence.

The yield expression and the return statement have both similarities and differences. The similarity is that both can return the value of the expression immediately following the statement. The difference is that every time it encounters yield, the function suspends execution, and the next time it continues to execute backwards from that position, the return statement does not have the function of position memory. In a function, you can only execute the return statement once (or one), 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 the origin of its name (in English, the word generator means "generator").

The Generator function can be used without the yield expression, at this time it becomes a purely 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 a normal function, it will be executed when assigning a value to the variable generator. However, the function f is a Generator function, so the function f will only be executed when the next method is called.

Also note that the yield expression can only be used in the Generator function, and an error will be reported when used in other places.

(function (){
  yield 1;
})()
// SyntaxError: Unexpected number

The above code uses the yield expression in an ordinary function, which results in a syntax error.

Here is another example.

var arr = [1, [[2, 3], 4], [5, 6]];

var flat = function* (a) {
  a.forEach(function (item) {
    if (typeof item !=='number') {
      yield* flat(item);
    } else {
      yield item;
    }
  });
};

for (var f of flat(arr)){
  console.log(f);
}

The above code will also produce a syntax error, because the parameter of the forEach method is a normal function, but the yield expression is used in it (this function also uses the yield* expression, see later for details) . One way to modify it is to use the for loop instead.

var arr = [1, [[2, 3], 4], [5, 6]];

var flat = function* (a) {
  var length = a.length;
  for (var i = 0; i < length; i++) {
    var item = a[i];
    if (typeof item !== "number") {
      yield* flat(item);
    } else {
      yield item;
    }
  }
};

for (var f of flat(arr)) {
  console.log(f);
}
// 1, 2, 3, 4, 5, 6

In addition, if the yield expression is used in another expression, it must be placed in parentheses.

function* demo() {
  console.log('Hello' + yield); // SyntaxError
  console.log('Hello' + yield 123); // SyntaxError

  console.log('Hello' + (yield)); // OK
  console.log('Hello' + (yield 123)); // OK
}

The yield expression is used as a function parameter or placed on the right side of an assignment expression, without parentheses.

function* demo() {
  foo(yield "a", yield "b"); // OK
  let input = yield; // OK
}

Relationship with Iterator interface

As mentioned in the previous chapter, the Symbol.iterator method of any object is equal to the iterator generating function of the object, and calling this function will return a iterator object of the object.

Since the Generator function is the iterator generation function, the Generator can be assigned to the object's Symbol.iterator property, so that the object has the Iterator interface.

var myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};

[...myIterable]; // [1, 2, 3]

In the above code, the Generator function is assigned to the Symbol.iterator property, so that the myIterable object has the Iterator interface and can be traversed by the ... operator.

After the Generator function is executed, it returns a iterator object. The object itself also has the Symbol.iterator property, and returns to itself after execution.

function* gen() {
  // some code
}

var g = gen();

g[Symbol.iterator]() === g;
// true

In the above code, gen is a Generator function, calling it will generate a traverser object g. Its Symbol.iterator property is also an iterator object generation function, which returns itself after execution.

Parameters of the next method

The yield expression itself does not return a value, or it always returns undefined. The next method can take a parameter, which will be used 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 indefinitely. If the next method has no parameters, every time it runs to the yield expression, the value of the variable reset is always undefined. When the next method takes a parameter true, the variable reset is reset to this parameter (that is, true), so i will be equal to -1, and the next cycle will start from -1 starts to increment.

This function has very important grammatical significance. Generator function from the pause state to resume running, its context state (context) is 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 to run. In other words, you can inject different values ​​from the outside to the inside at different stages of the generator function to adjust the function's behavior.

Let's look at another example.

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, when the next method is run for the second time without parameters, the value of y is equal to 2 * undefined (ie NaN). After dividing by 3, it is still NaN, so the value of the object is returned The attribute is also equal to NaN. There are no parameters when the Next method is run for the third time, so z is equal to undefined, and the value property of the returned object is equal to 5 + NaN + undefined, that is, NaN.

If you provide parameters to the next method, the result will be 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 call to the next method sets the value of the previous yield expression to 12, so y is equal to 24, returning the value of y / 3 8; calling the next method for the third time, setting the value of the previous yield expression to 13, so z is equal to 13, then x is equal to 5 and 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, it is invalid to pass the parameters when the next method is used for the first time. The V8 engine directly ignores the parameters when the next method is used for the first time, and the parameters are valid only from the second use of the next method. Semantically speaking, the first next method is used to start the iterator object, so there is no need to take parameters.

Let's look at an example of inputting values ​​into the Generator function through the parameters of the next method.

function* dataConsumer() {
  console.log("Started");
  console.log(`1. ${yield}`);
  console.log(`2. ${yield}`);
  return "result";
}

let genObj = dataConsumer();
genObj.next();
// Started
genObj.next("a");
// 1. a
genObj.next("b");
// 2. b

The above code is a very intuitive example, each time you enter a value into the Generator function through the next method, and then print it out.

If you want to be able to enter the value when you call the next method for the first time, you can wrap another layer outside the Generator function.

function wrapper(generatorFunction) {
  return function (...args) {
    let generatorObject = generatorFunction(...args);
    generatorObject.next();
    return generatorObject;
  };
}

const wrapped = wrapper(function* () {
  console.log(`First input: ${yield}`);
  return "DONE";
});

wrapped().next("hello!");
// First input: hello!

In the above code, if the Generator function does not need to wrap a layer first with wrapper, it is impossible to call the next method for the first time and input parameters.

for...of loop

The for...of loop can automatically traverse the Iterator object generated when the Generator function is running, and there is no need to call the next method at this time.

function* foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5

The above code uses the for...of loop to display the values ​​of 5 yield expressions in sequence. It should be noted here that once the done attribute of the return object of the next method is true, the for...of loop will be terminated and the return object is not included, so the return statement of the above code returns The 6 is not included in the for...of loop.

The following is an example of using the Generator function and the for...of loop to realize the Fibonacci sequence.

function* fibonacci() {
  let [prev, curr] = [0, 1];
  for (;;) {
    yield curr;
    [prev, curr] = [curr, prev + curr];
  }
}

for (let n of fibonacci()) {
  if (n > 1000) break;
  console.log(n);
}

As can be seen from the above code, the next method is not required when using the for...of statement.

Using the for...of loop, you can write a method to traverse any object. The native JavaScript object does not have a traversal interface and cannot use the for...of loop. Add this interface to it through the Generator function, and it can be used.

function* objectEntries(obj) {
  let propKeys = Reflect.ownKeys(obj);

  for (let propKey of propKeys) {
    yield [propKey, obj[propKey]];
  }
}

let jane = { first: "Jane", last: "Doe" };

for (let [key, value] of objectEntries(jane)) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

In the above code, the object jane does not natively have the Iterator interface and cannot be traversed with for...of. At this time, we add a traverser interface to it through the Generator function objectEntries, and it can be traversed with for...of. Another way to add the iterator interface is to add the Generator function to the Symbol.iterator property of the object.

function* objectEntries() {
  let propKeys = Object.keys(this);

  for (let propKey of propKeys) {
    yield [propKey, this[propKey]];
  }
}

let jane = { first: "Jane", last: "Doe" };

jane[Symbol.iterator] = objectEntries;

for (let [key, value] of jane) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

Except for the for...of loop, the spread operator (...), destructuring assignment, and the internal calls of the Array.from method are all iterator interfaces. This means that they can all take the Iterator object returned by the Generator function as a parameter.

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

// spread operator
[...numbers()]; // [1, 2]

// Array.from method
Array.from(numbers()); // [1, 2]

// Destructuring assignment
let [x, y] = numbers();
x; // 1
y; // 2

// for...of loop
for (let n of numbers()) {
  console.log(n);
}
// 1
// 2

Generator.prototype.throw()

The iterator object returned by the Generator function has a throw method, which can throw an error outside of the function and then catch it in the body of the Generator function.

var g = function* () {
  try {
    yield;
  } catch (e) {
    console.log("Internal capture", e);
  }
};

var i = g();
i.next();

try {
  i.throw("a");
  i.throw("b");
} catch (e) {
  console.log("External capture", e);
}
// internal capture a
// external capture b

In the above code, the iterator object i throws two consecutive errors. The first error is caught by the catch statement in the generator function body. i throws an error for the second time. Since the catch statement inside the Generator function has been executed, this error will no longer be caught, so this error is thrown into the body of the Generator function, which is outside the function. The catch statement catches.

The throw method can accept a parameter, which will be received by the catch statement. It is recommended to throw an instance of the Error object.

var g = function* () {
  try {
    yield;
  } catch (e) {
    console.log(e);
  }
};

var i = g();
i.next();
i.throw(new Error("Error!"));
// Error: Something went wrong! (...)

Be careful not to confuse the throw method of the iterator object with the global throw command. The error in the above code is thrown by the throw method of the iterator object, not by the throw command. The latter can only be caught by the catch statement outside the function.

var g = function* () {
  while (true) {
    try {
      yield;
    } catch (e) {
      if (e != "a") throw e;
      console.log("Internal capture", e);
    }
  }
};

var i = g();
i.next();

try {
  throw new Error("a");
  throw new Error("b");
} catch (e) {
  console.log("External capture", e);
}
// External capture [Error: a]

The reason why the above code only captures a is because the catch statement block outside of the function captures the thrown a error, and it will not continue with the remaining statements in the try code block.

If the try...catch code block is not deployed inside the Generator function, the error thrown by the throw method will be caught by the external try...catch code block.

var g = function* () {
  while (true) {
    yield;
    console.log("Internal capture", e);
  }
};

var i = g();
i.next();

try {
  i.throw("a");
  i.throw("b");
} catch (e) {
  console.log("External capture", e);
}
// external capture a

In the above code, the try...catch code block is not deployed inside the Generator function g, so the thrown error is directly caught by the external catch code block.

If the try...catch code block is not deployed inside or outside the Generator function, the program will report an error and directly interrupt the execution.

var gen = function* gen() {
  yield console.log("hello");
  yield console.log("world");
};

var g = gen();
g.next();
g.throw();
// hello
// Uncaught undefined

In the above code, after g.throw throws an error, there is no try...catch code block to catch this error, causing the program to report an error and interrupt execution.

Errors thrown by the throw method must be caught internally, provided that the next method must be executed at least once.

function* gen() {
  try {
    yield 1;
  } catch (e) {
    console.log("Internal capture");
  }
}

var g = gen();
g.throw(1);
// Uncaught 1

In the above code, when g.throw(1) is executed, the next method has not been executed once. At this time, the thrown error will not be caught internally, but will be thrown directly externally, causing a program error. This behavior is actually very easy to understand, because the first execution of the next method is equivalent to starting the execution of the internal code of the Generator function, otherwise the Generator function has not started execution, then the throwing error of the throw method can only be thrown in the function external.

After the throw method is captured, the next yield expression will be executed. In other words, the next method will be executed once.

var gen = function* gen() {
  try {
    yield console.log("a");
  } catch (e) {
    // ...
  }
  yield console.log("b");
  yield console.log("c");
};

var g = gen();
g.next(); // a
g.throw(); // b
g.next(); // c

In the above code, after the g.throw method is captured, the next method is automatically executed once, so b will be printed. In addition, you can also see that as long as the try...catch code block is deployed inside the Generator function, the error thrown by the throw method of the traverser will not affect the next traversal.

In addition, the throw command and the g.throw method have nothing to do with each other.

var gen = function* gen() {
  yield console.log("hello");
  yield console.log("world");
};

var g = gen();
g.next();

try {
  throw new Error();
} catch (e) {
  g.next();
}
// hello
// world

In the above code, the error thrown by the throw command will not affect the state of the traverser, so the correct operation is performed when the next method is executed twice.

This mechanism of trapping errors in the function body greatly facilitates the handling of errors. Multiple yield expressions, you can use only one try...catch code block to catch errors. If you use the writing method of the callback function and want to catch multiple errors, you have to write an error handling statement inside each function. Now, you only need to write the catch statement once inside the Generator function.

Errors thrown outside the generator function body can be caught in the function body; conversely, errors thrown out of the generator function body can also be caught by catch outside the function body.

function* foo() {
  var x = yield 3;
  var y = x.toUpperCase();
  yield y;
}

var it = foo();

it.next(); // {value:3, done:false}

try {
  it.next(42);
} catch (err) {
  console.log(err);
}

In the above code, the second next method passes a parameter 42 into the function body. The value does not have the toUpperCase method, so a TypeError error will be thrown, which is caught by the catch outside the function body.

Once the generator throws an error during execution and is not caught internally, it will not execute anymore. If the next method is called afterwards, an object with the value property equal to undefined and the done property equal to true will be returned, that is, the JavaScript engine thinks that this Generator has finished running.

function* g() {
  yield 1;
  console.log("throwing an exception");
  throw new Error("generator broke!");
  yield 2;
  yield 3;
}

function log(generator) {
  var v;
  console.log("starting generator");
  try {
    v = generator.next();
    console.log("Run next method for the first time", v);
  } catch (err) {
    console.log("catch error", v);
  }
  try {
    v = generator.next();
    console.log("Run next method for the second time", v);
  } catch (err) {
    console.log("catch error", v);
  }
  try {
    v = generator.next();
    console.log("Run next method for the third time", v);
  } catch (err) {
    console.log("catch error", v);
  }
  console.log("caller done");
}

log(g());
// starting generator
// Run the next method for the first time {value: 1, done: false}
// throwing an exception
// Catch errors {value: 1, done: false}
// Run the next method for the third time {value: undefined, done: true}
// caller done

The above code runs the next method a total of three times, and an error will be thrown the second time it is run, and then when it is run the third time, the Generator function has ended and will no longer be executed.

Generator.prototype.return()

The iterator object returned by the Generator function also has a return() method, which can return a given value and terminate the traversal of the Generator function.

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

var g = gen();

g.next(); // {value: 1, done: false}
g.return("foo"); // {value: "foo", done: true}
g.next(); // {value: undefined, done: true}

In the above code, after the traverser object g calls the return() method, the value attribute of the return value is the parameter foo of the return() method. Moreover, the traversal of the Generator function is terminated, the done attribute of the return value is true, and the next() method is called later, the done attribute always returns true.

If the return() method is called without providing parameters, the value property of the return value is undefined.

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

var g = gen();

g.next(); // {value: 1, done: false}
g.return(); // {value: undefined, done: true}

If there is a try...finally code block inside the Generator function, and the try code block is being executed, then the return() method will immediately enter the finally code block, and the entire function will end after execution .

function* numbers() {
  yield 1;
  try {
    yield 2;
    yield 3;
  } finally {
    yield 4;
    yield 5;
  }
  yield 6;
}
var g = numbers();
g.next(); // {value: 1, done: false}
g.next(); // {value: 2, done: false}
g.return(7); // {value: 4, done: false}
g.next(); // {value: 5, done: false}
g.next(); // {value: 7, done: true}

In the above code, after calling the return() method, it starts to execute the finally code block without executing the remaining code in try, and then waits until the finally code block is executed, and then returns to return() The return value specified by the method.

The common ground of next(), throw(), return()

The three methods next(), throw(), and return() are essentially the same thing and can be understood together. Their role is to resume the execution of the Generator function and replace the yield expression with a different statement.

next() replaces the yield expression with a value.

const g = function* (x, y) {
  let result = yield x + y;
  return result;
};

const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}

gen.next(1); // Object {value: 1, done: true}
// Equivalent to let result = yield x + y
// Replace with let result = 1;

In the above code, the second next(1) method is equivalent to replacing the yield expression with a value of 1. If the next method has no parameters, it is equivalent to replacing it with undefined.

throw() replaces the yield expression with a throw statement.

gen.throw(new Error("Error")); // Uncaught Error: Error
// Equivalent to let result = yield x + y
// Replace with let result = throw(new Error('Error'));

return() replaces the yield expression with a return statement.

gen.return(2); // Object {value: 2, done: true}
// Equivalent to let result = yield x + y
// Replace with let result = return 2;

yield* expression

If inside the Generator function, call another Generator function. You need to manually complete the traversal within the body of the former function.

function* foo() {
  yield "a";
  yield "b";
}

function* bar() {
  yield "x";
  // Manually traverse foo()
  for (let i of foo()) {
    console.log(i);
  }
  yield "y";
}

for (let v of bar()) {
  console.log(v);
}
// x
// a
// b
// y

In the above code, both foo and bar are Generator functions. Calling foo in bar requires manual traversal of foo. If there are multiple Generator functions nested, it is very troublesome to write.

ES6 provides yield* expressions as a solution to execute another Generator function inside a Generator function.

function* bar() {
  yield "x";
  yield* foo();
  yield "y";
}

// Equivalent to
function* bar() {
  yield "x";
  yield "a";
  yield "b";
  yield "y";
}

// Equivalent to
function* bar() {
  yield "x";
  for (let v of foo()) {
    yield v;
  }
  yield "y";
}

for (let v of bar()) {
  console.log(v);
}
// "x"
// "a"
// "b"
// "y"

Let's look at another comparative example.

function* inner() {
  yield "hello!";
}

function* outer1() {
  yield "open";
  yield inner();
  yield "close";
}

var gen = outer1();
gen.next().value; // "open"
gen.next().value; // returns a iterator object
gen.next().value; // "close"

function* outer2() {
  yield "open";
  yield* inner();
  yield "close";
}

var gen = outer2();
gen.next().value; // "open"
gen.next().value; // "hello!"
gen.next().value; // "close"

In the above example, yield* is used in outer2, but not in outer1. The result is that outer1 returns a iterator object, and outer2 returns the internal value of the iterator object.

From a grammatical point of view, if the yield expression is followed by a iterator object, you need to add an asterisk after the yield expression to indicate that it returns a iterator object. This is called the yield* expression.

let delegatedIterator = (function* () {
  yield "Hello!";
  yield "Bye!";
})();

let delegatingIterator = (function* () {
  yield "Greetings!";
  yield* delegatedIterator;
  yield "Ok, bye.";
})();

for (let value of delegatingIterator) {
  console.log(value);
}
// "Greetings!
// "Hello!"
// "Bye!"
// "Ok, bye."

In the above code, delegatingIterator is the delegate, and delegatedIterator is the delegate. Since the value obtained by the yield* delegatedIterator statement is an iterator, it should be represented by an asterisk. The result of the operation is to use one traverser to traverse multiple Generator functions, which has a recursive effect.

The Generator function after yield* (when there is no return statement) is equivalent to deploying a for...of loop inside the Generator function.

function* concat(iter1, iter2) {
  yield* iter1;
  yield* iter2;
}

// Equivalent to

function* concat(iter1, iter2) {
  for (var value of iter1) {
    yield value;
  }
  for (var value of iter2) {
    yield value;
  }
}

The above code shows that the Generator function behind yield* (when there is no return statement) is just a shorthand form of for...of, and the latter can be used instead of the former. Conversely, when there is a return statement, you need to use the form of var value = yield* iterator to get the value of the return statement.

If yield* is followed by an array, the array members will be traversed because the array natively supports iterators.

function* gen() {
  yield* ["a", "b", "c"];
}

gen().next(); // {value:"a", done:false}

In the above code, if there is no asterisk after the yield command, the entire array is returned, and the asterisk is added to indicate that the iterator object of the array is returned.

In fact, any data structure can be traversed by yield* as long as it has an Iterator interface.

let read = (function* () {
  yield "hello";
  yield* "hello";
})();

read.next().value; // "hello"
read.next().value; // "h"

In the above code, the yield expression returns the entire string, and the yield* statement returns a single character. Because the string has an Iterator interface, it is traversed by yield*.

If the proxy generator function has a return statement, then data can be returned to the proxy generator function.

function* foo() {
  yield 2;
  yield 3;
  return "foo";
}

function* bar() {
  yield 1;
  var v = yield* foo();
  console.log("v: " + v);
  yield 4;
}

var it = bar();

it.next();
// {value: 1, done: false}
it.next();
// {value: 2, done: false}
it.next();
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next();
// {value: undefined, done: true}

When the above code calls the next method for the fourth time, there will be output on the screen. This is because the return statement of the function foo provides a return value to the function bar.

Let's look at another example.

function* genFuncWithReturn() {
  yield "a";
  yield "b";
  return "The result";
}
function* logReturned(genObj) {
  let result = yield* genObj;
  console.log(result);
}

[...logReturned(genFuncWithReturn())];
// The result
// Value is ['a','b']

In the above code, there are two traversals. The first time is the iterator object returned by the spread operator traversal function logReturned, and the second time is the iterator object returned by the yield* statement traversal function genFuncWithReturn. The effects of these two traversals are superimposed, and finally manifested as the traverser object returned by the spread operator traversal function genFuncWithReturn. Therefore, the value obtained by the final data expression is equal to ['a','b' ]. However, the return value The result of the return statement of the function genFuncWithReturn will be returned to the result variable inside the function logReturned, so there will be terminal output.

The yield* command can easily extract all members of a nested array.

function* iterTree(tree) {
  if (Array.isArray(tree)) {
    for (let i = 0; i < tree.length; i++) {
      yield* iterTree(tree[i]);
    }
  } else {
    yield tree;
  }
}

const tree = ["a", ["b", "c"], ["d", "e"]];

for (let x of iterTree(tree)) {
  console.log(x);
}
// a
// b
// c
// d
// e

Since the spread operator ... calls the Iterator interface by default, the above function can also be used for tiling nested arrays.

[...iterTree(tree)]; // ["a", "b", "c", "d", "e"]

The following is a slightly more complicated example, using the yield* statement to traverse a complete binary tree.

// Below is the constructor of the binary tree,
// The three parameters are the left tree, the current node and the right tree
function Tree(left, label, right) {
  this.left = left;
  this.label = label;
  this.right = right;
}

// Below is the inorder traversal function.
// Since the return is a traverser, the generator function is used.
// The function body uses a recursive algorithm, so the left tree and the right tree should be traversed with yield*
function* inorder(t) {
  if (t) {
    yield* inorder(t.left);
    yield t.label;
    yield* inorder(t.right);
  }
}

// Generate a binary tree below
function make(array) {
  // Determine whether it is a leaf node
  if (array.length == 1) return new Tree(null, array[0], null);
  return new Tree(make(array[0]), array[1], make(array[2]));
}
let tree = make([[["a"], "b", ["c"]], "d", [["e"], "f", ["g"]]]);

// Traverse the binary tree
var result = [];
for (let node of inorder(tree)) {
  result.push(node);
}

result;
// ['a','b','c','d','e','f','g']

Generator function as an object property

If the property of an object is a Generator function, it can be abbreviated as the following form.

let obj = {
  * myGeneratorMethod() {
    ···
  }
};

In the above code, there is an asterisk in front of the myGeneratorMethod property, indicating that this property is a Generator function.

Its complete form is as follows, which is equivalent to the above writing.

let obj = {
  myGeneratorMethod: function* () {
    // ···
  },
};

Generator function this

The Generator function always returns a iterator. ES6 stipulates that this iterator is an instance of the Generator function and also inherits the methods on the prototype object of the Generator function.

function* g() {}

g.prototype.hello = function () {
  return "hi!";
};

let obj = g();

obj instanceof g; // true
obj.hello(); //'hi!'

The above code shows that the iterator obj returned by the Generator function g is an instance of g and inherits g.prototype. However, if you treat g as a normal constructor, it won't work, because g always returns the iterator object, not the this object.

function* g() {
  this.a = 11;
}

let obj = g();
obj.next();
obj.a; // undefined

In the above code, the Generator function g adds an attribute a to the this object, but the obj object cannot get this attribute.

The Generator function cannot be used with the new command, and an error will be reported.

function* F() {
  yield (this.x = 2);
  yield (this.y = 3);
}

new F();
// TypeError: F is not a constructor

In the above code, the new command is used together with the constructor F, and an error is reported because F is not a constructor.

So, is there a way to make the Generator function return a normal object instance, either by using the next method, but also by getting the normal this?

Here is a workaround. First, generate an empty object and use the call method to bind the this inside the Generator function. In this way, after the constructor is called, this empty object is the instance object of the Generator function.

function* F() {
  this.a = 1;
  yield (this.b = 2);
  yield (this.c = 3);
}
var obj = {};
var f = F.call(obj);

f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}

obj.a; // 1
obj.b; // 2
obj.c; // 3

In the above code, first the this object inside F binds the obj object, and then calls it to return an Iterator object. This object executes the next method three times (because there are two yield expressions inside F) to complete the execution of all code inside F. At this time, all internal properties are bound to the obj object, so the obj object becomes an instance of F.

In the above code, the iterator object f is executed, but the generated object instance is obj. Is there a way to unify these two objects?

One way is to replace obj with F.prototype.

function* F() {
  this.a = 1;
  yield (this.b = 2);
  yield (this.c = 3);
}
var f = F.call(F.prototype);

f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}

fa; // 1
fb; // 2
fc; // 3

Then change F to a constructor, and you can execute the new command on it.

function* gen() {
  this.a = 1;
  yield (this.b = 2);
  yield (this.c = 3);
}

function F() {
  return gen.call(gen.prototype);
}

var f = new F();

f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}

fa; // 1
fb; // 2
fc; // 3

Meaning

Generator and State Machine

Generator is the best structure to implement state machine. For example, the following clock function is a state machine.

var ticking = true;
var clock = function () {
  if (ticking) console.log("Tick!");
  else console.log("Tock!");
  ticking = !ticking;
};

The clock function of the above code has two states (Tick and Tock). Each time it runs, it changes the state. If this function is implemented with Generator, it is as follows.

var clock = function* () {
  while (true) {
    console.log("Tick!");
    yield;
    console.log("Tock!");
    yield;
  }
};

The above generator implementation is compared with the ES5 implementation. It can be seen that there is no external variable ticking used to save the state, which is more concise, safer (the state will not be illegally tampered with), and is more in line with the idea of ​​functional programming. The writing is also more elegant. Generator can save state without external variables because it contains a state information, that is, whether it is currently in a pause state.

Generator and Coroutine

Coroutine is a way of program operation, which can be understood as "cooperative thread" or "cooperative function". The coroutine can be implemented with a single thread or with multiple threads. The former is a special subroutine, and the latter is a special thread.

(1) The difference between coroutine and subroutine

The traditional "subroutine" (subroutine) adopts a stack-style "last-in first-out" execution method. Only when the called sub-function is completely executed, will the execution of the parent function end. The coroutine is different from it, multiple threads (in the case of a single thread, that is, multiple functions) can be executed in parallel, but only one thread (or function) is in a running state, and other threads (or functions) are in a suspended state (suspended) , Threads (or functions) can exchange execution rights. That is to say, a thread (or function) can be executed halfway through, and execution can be suspended, and execution rights can be handed over to another thread (or function), and execution can be resumed when the execution rights are later withdrawn. Such threads (or functions) that can execute in parallel and exchange execution rights are called coroutines.

From the implementation point of view, in the memory, the subroutine uses only one stack (stack), while the coroutine has multiple stacks at the same time, but only one stack is in the running state, that is, the coroutine takes up more memory At the expense of achieving multi-task parallelism.

(2) The difference between coroutine and ordinary thread

It is not difficult to see that the coroutine is suitable for the environment of multitasking. In this sense, it is very similar to a normal thread. It has its own execution context and can share global variables. The difference between them is that multiple threads can be running at the same time, but there can only be one running coroutine, and all other coroutines are in a suspended state. In addition, ordinary threads are preemptive, and which thread gets the resources first must be determined by the operating environment, but the coroutine is cooperative, and the execution right is allocated by the coroutine itself.

Since JavaScript is a single-threaded language, only one call stack can be maintained. After the introduction of the coroutine, each task can maintain its own call stack. The biggest advantage of this is that when an error is thrown, the original call stack can be found. Not like the callback function of asynchronous operation, once an error occurs, the original call stack will end long ago.

The Generator function is ES6's implementation of the coroutine, but it is incomplete. Generator function is called "semi-coroutine" (semi-coroutine), which means that only the caller of the Generator function can return the execution power of the program to the Generator function. If it is a fully executed coroutine, any function can let the suspended coroutine continue to execute.

If you treat the Generator function as a coroutine, you can write multiple tasks that need to cooperate with each other as a Generator function, and use the yield expression to exchange control between them.

Generator and context

When JavaScript code is running, it will generate a global context (context, also known as the running environment), which contains all the current variables and objects. Then, when the function (or block-level code) is executed, a context in which the function runs will be generated in the upper layer of the current context environment, which becomes the current (active) context, thus forming a context stack (context stack) .

This stack is a "last-in, first-out" data structure. The resulting context is executed first, exits the stack, and then executes the context below it, until all code execution is completed and the stack is cleared.

This is not the case with the Generator function. It executes the generated context. Once it encounters the yield command, it will temporarily exit the stack, but it will not disappear. All variables and objects in it will be frozen in the current state. When the next command is executed on it, this context will be added to the call stack again, and the frozen variables and objects will resume execution.

function* gen() {
  yield 1;
  return 2;
}

let g = gen();

console.log(g.next().value, g.next().value);

In the above code, when g.next() is executed for the first time, the context of the Generator function gen will be added to the stack, that is, the code inside gen will start to run. When encountering yield 1, the gen context exits the stack and the internal state is frozen. When g.next() is executed for the second time, the gen context is added to the stack again and becomes the current context, and execution resumes.

Application

Generator can pause function execution and return the value of any expression. This feature makes Generator have a variety of application scenarios.

(1) Synchronized expression of asynchronous operation

The effect of the pause execution of the Generator function means that asynchronous operations can be written in the yield expression, and then executed later when the next method is called. This is actually equivalent to no need to write a callback function, because the subsequent operation of the asynchronous operation can be placed under the yield expression, anyway, it will not be executed until the next method is called. Therefore, an important practical significance of the Generator function is to handle asynchronous operations and rewrite the callback function.

function* loadUI() {
  showLoadingScreen();
  yield loadUIDataAsynchronously();
  hideLoadingScreen();
}
var loader = loadUI();
// Load UI
loader.next();

// Uninstall UI
loader.next();

In the above code, when the loadUI function is called for the first time, the function will not be executed, only a traverser will be returned. The next time the next method is called on the traverser, the Loading interface (showLoadingScreen) will be displayed, and the data will be loaded asynchronously (loadUIDataAsynchronously). When the data is loaded, use the next method again to hide the Loading interface. As you can see, the advantage of this way of writing is that all the logic of the Loading interface is encapsulated in a function, which is very clear step by step.

Ajax is a typical asynchronous operation. The Ajax operation deployed through the Generator function can be expressed in a synchronous manner.

function* main() {
  var result = yield request("http://some.url");
  var resp = JSON.parse(result);
  console.log(resp.value);
}

function request(url) {
  makeAjaxCall(url, function (response) {
    it.next(response);
  });
}

var it = main();
it.next();

The main function of the above code is to obtain data through Ajax operations. As you can see, except for the addition of a yield, it is almost exactly the same as the synchronous operation. Note that in the next method in the makeAjaxCall function, the response parameter must be added, because the yield expression itself has no value and is always equal to undefined.

The following is another example of reading a text file line by line through the Generator function.

function* numbers() {
  let file = new FileReader("numbers.txt");
  try {
    while (!file.eof) {
      yield parseInt(file.readLine(), 10);
    }
  } finally {
    file.close();
  }
}

The above code opens a text file, use the yield expression to manually read the file line by line.

(2) Control flow management

If there is a multi-step operation that is very time-consuming, a callback function may be used, which may be written as follows.

step1(function (value1) {
  step2(value1, function (value2) {
    step3(value2, function (value3) {
      step4(value3, function (value4) {
        // Do something with value4
      });
    });
  });
});

Rewrite the above code with Promise.

Promise.resolve(step1)
  .then(step2)
  .then(step3)
  .then(step4)
  .then(
    function (value4) {
      // Do something with value4
    },
    function (error) {
      // Handle any error from step1 through step4
    }
  )
  .done();

The above code has changed the callback function into a linear execution form, but added a lot of Promise syntax. The Generator function can further improve the code running process.

function* longRunningTask(value1) {
  try {
    var value2 = yield step1(value1);
    var value3 = yield step2(value2);
    var value4 = yield step3(value3);
    var value5 = yield step4(value4);
    // Do something with value4
  } catch (e) {
    // Handle any error from step1 through step4
  }
}

Then, use a function to automatically execute all the steps in order.

scheduler(longRunningTask(initialValue));

function scheduler(task) {
  var taskObj = task.next(task.value);
  // If the Generator function is not over, continue to call
  if (!taskObj.done) {
    task.value = taskObj.value;
    scheduler(task);
  }
}

Note that the above approach is only suitable for synchronous operations, that is, all `tasks' must be synchronous, and there can be no asynchronous operations. Because the code here continues to execute as soon as it gets the return value, without judging when the asynchronous operation is completed. If you want to control the asynchronous operation process, see the chapter "Asynchronous Operation" for details.

Next, use the feature of for...of loops to automatically execute yield commands in sequence to provide a more general method of control flow management.

let steps = [step1Func, step2Func, step3Func];

function* iterateSteps(steps) {
  for (var i = 0; i < steps.length; i++) {
    var step = steps[i];
    yield step();
  }
}

In the above code, the array steps encapsulates multiple steps of a task, and the generator function iterateSteps adds the yield command to these steps in turn.

After decomposing the task into steps, you can also decompose the project into multiple tasks that are executed sequentially.

let jobs = [job1, job2, job3];

function* iterateJobs(jobs) {
  for (var i = 0; i < jobs.length; i++) {
    var job = jobs[i];
    yield* iterateSteps(job.steps);
  }
}

In the above code, the array jobs encapsulates multiple tasks of a project, and the generator function iterateJobs adds yield* commands to these tasks in turn.

Finally, you can use the for...of loop to execute all the steps of all tasks one at a time.

for (var step of iterateJobs(jobs)) {
  console.log(step.id);
}

Again, the above approach can only be used when all steps are synchronous operations, and there can be no asynchronous operations. If you want to execute asynchronous steps in sequence, you must use the method described in the following chapter "Asynchronous Operations".

The essence of for...of is a while loop, so the above code essentially executes the following logic.

var it = iterateJobs(jobs);
var res = it.next();

while (!res.done) {
  var result = res.value;
  // ...
  res = it.next();
}

(3) Deploy the Iterator interface

Using the Generator function, you can deploy the Iterator interface on any object.

function* iterEntries(obj) {
  let keys = Object.keys(obj);
  for (let i = 0; i < keys.length; i++) {
    let key = keys[i];
    yield [key, obj[key]];
  }
}

let myObj = { foo: 3, bar: 7 };

for (let [key, value] of iterEntries(myObj)) {
  console.log(key, value);
}

// foo 3
// bar 7

In the above code, myObj is an ordinary object, through the iterEntries function, there is an Iterator interface. In other words, the next method can be deployed on any object.

The following is an example of deploying the Iterator interface to an array, even though the array has this interface natively.

function* makeSimpleGenerator(array) {
  var nextIndex = 0;

  while (nextIndex < array.length) {
    yield array[nextIndex++];
  }
}

var gen = makeSimpleGenerator(["yo", "ya"]);

gen.next().value; //'yo'
gen.next().value; //'ya'
gen.next().done; // true

(4) As a data structure

Generator can be regarded as a data structure, more precisely, it can be regarded as an array structure, because the Generator function can return a series of values, which means that it can provide an array-like interface for any expression.

function* doStuff() {
  yield fs.readFile.bind(null, "hello.txt");
  yield fs.readFile.bind(null, "world.txt");
  yield fs.readFile.bind(null, "and-such.txt");
}

The above code returns three functions in turn, but due to the use of the Generator function, the three returned functions can be processed like an array.

for (task of doStuff()) {
  // task is a function, you can use it like a callback function
}

In fact, if you use ES5 to express, you can use an array to simulate this usage of Generator.

function doStuff() {
  return [
    fs.readFile.bind(null, "hello.txt"),
    fs.readFile.bind(null, "world.txt"),
    fs.readFile.bind(null, "and-such.txt"),
  ];
}

The above function can be processed in exactly the same for...of loop! Comparing the two phases, it is not difficult to see that the Generator enables data or operations with an array-like interface.