post-css-position

JavaScript Generator: Amazing Tips that You Never Know (2019)

Introduction

Basic concept

The JavaScript Generator function is an asynchronous programming solution provided by ES6, and its syntax behavior is completely different from traditional functions. This post details the syntax and API of the Generator function. For its asynchronous programming application, see the article “Asynchronous Application of the Generator Function”.

The Generator function can be understood from a variety of perspectives. Grammatically, the Generator function is a state machine that encapsulates multiple internal states.

Executing the Generator function generates a traversal object. The returned object, which traverses each state inside the Generator function sequentially.

Formally, the Generator function is a normal function, but has two features.

  • First, there is an asterisk between the keyword function and the function name.
  • Second, the function body internally uses the yield expression to define different internal states (In English, yield means “output”).
function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

The above code defines a Generator function helloWorldGenerator with two yield expressions ( hello and world) inside it. Thus, the function has three states: hello, world, and return statement (end execution).

Then, the Generator function is called in the same way as a normal function, with a pair of parentheses after the function name. The difference is that after invocation the Generator function, the function is not executed. The returned result is not the function’s running result, but a pointer object pointing to the internal state, which is an Iterator Object introduced in the previous article.

Next, you must call the next method of the Iterator object to move the pointer to the next state. That is, each time the next method is called , the internal pointer is executed from the function’s head or where it was last stopped until the next yield expression (or return statement) is encountered. In other words, the Generator function is executed in segments, the yield expression is a marker that suspends 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 has called the next method four times.

The first time the call is made, the Generator function begins execution until the first yield expression is encountered. The next method returns an object whose value property is the value hello of the current yield expression, and the done property is false, indicating that the traversal has not ended.

On the second call, the Generator function executes from the last expression stop to the next yield expression. The value property returned by the method is the value world of the current yield expression, and the value of the done property is still false indicates that the traversal has not ended.

On the third call, the Generator function executes from the last yield expression to the return statement (if there is no return statement, it executes to the end of the function). The value property returned by the method is the value of the return expression (if there is no return statement, value becomes undefined). The value of the done property is true, meaning that the traversal has ended.

The fourth call, when the Generator function has finished running, the next method returns undefined as the value property, and true as the done property. All later invocation of the next method, will always output this object.

To summarize, invocation of the Generator function returns an Iterator object that represents the internal pointer of the Generator function. Traversing the Iterator object with the next method. Each time an object (with two properties value and done) is returned.

  • The value attribute represents the value of the current internal state, which is the value of the yield or return expressions.
  • The done attribute is a Boolean value indicating whether the traversal is over.

ES6 does not specify where the asterisk between the keyword function and the function name is written. This leads to the following styles.

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

Since the Generator function is still a normal function, the general way of writing is the third one above, that is, the asterisk follows the function keyword. We will use this method of writing from now on.

Yield expression

Only invocation of the next method will traverse a Generator function’s internal state, so it 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 a yield expression is encountered , the subsequent operation is suspended, and the value of the yield expression is used as the value of the returned object .
  2. When the next method is called next time, the function continues execution until the next yield expression is encountered .
  3. If no new yield expression is encountered , it will run until the end of the function or until the return statement, and the value of the return expression is taken as the value of the returned object .
  4. If the function has no return statement, the value of the returned object is undefined.

It should be noted that the expressions after 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 the manual “Lazy Evaluation” syntax for JavaScript.

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

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

The yield and return statements have similarities and differences. The similarity is that you can return a value with both expressions. The difference is that each time the yield statement is encountered , the function pauses execution, and the next time it continues from the position.

The return statement does not have the feature of position memory. In a function, you can only execute one return statement, but you can execute multiple yield expressions. A normal function can only return one value because it can only execute return once; the Generator function 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.

The Generator function can be used without yield expression, and 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 a normal function, it will be executed immediately when assigning  f() to the variable generator. However, the function f is a Generator function, and the function f is executed only when the next method is called.

Also note that the yield expression can only be used in the Generator function, otherwise an error will be reported.

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

The above code uses a yield expression in a normal function , resulting in a syntax error.

Here’s 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 generate a syntax error, because the argument to the forEach method is a normal function, but the yield expression is used inside. We also use the yield* expression, which will be described later. We modify above codes as follows.

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 inside the 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 can be used as function arguments or on the right side of an assignment expression, without parentheses.

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

Relationship with the Iterator interface

Any object’s Symbol.iterator method is equal to the object’s generation function, and calling this function returns an Iterator object of that object.

Since the Generator function generates an Iterator object, you can assign the Generator to an object’s Symbol.iterator property, making the object have an 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 an Iterator interface that can be traversed by the ... operator.

After the execution of the Generator function, it returns an Iterator object. The object itself also has the Symbol.iterator property that returns itself after execution.

function* gen(){
  // some code
}

var g = gen();

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

In the above code, gen is a Generator function, which is called to generate a Generator object g. The execution of g[Symbol.iterator]() returns itself.

Parameters of the next method

The yield expression itself has no return value, or always returns undefined. The next method can take one argument, which is 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, the value of the reset variable is always undefined each time it runs to the yield expression. When a next method takes a parameter true, the variable reset is reset to this parameter (i.e., true), so i will increase from -1.

This feature has important grammatical implications. The Generator function runs from a paused state to a resumed state, and its context is unchanged. Through the parameters of the next method, there is a way to continue to inject values ​​into the body of the function after the Generator function starts running. That is, you can adjust the behavior of the function by injecting different values ​​from the outside to the inside at different stages of the Generator function.

Look at an 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, the second time you run the next method without parameters, the value of y is equal to 2 * undefined (i.e., NaN). Divided by 3 obtains NaN. So the value property of the returned object are also equal to NaN. The third time the next method is run without parameters, so z equal to undefined, the value property of the returned object is equal to 5 + NaN + undefined, i.e., NaN.

If you supply parameters to the next method, the result is completely different. The second invocation of the b.next method is provided with a number parameter 12, the value of the last yield expression is set to 12. So y equals to 2*12=24. The third time the b.next method is called, the value of the last yield expression is set to 13, therefore z equals to 13. So the final return value is 5+24+13=42.

Note that since the next method’s argument represents the return value of the previous yield expression, passing the argument is invalid when the next method is first used. The V8 engine directly ignores the parameters when using the next method for the first time. The parameters are valid only from the second invocation of the next method. Semantically, the first next method is used to start the Generator object, so there is no need to take parameters.

Look at an example of passing a value to the next method and inputting a value into the Generator function.

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 straightforward example, each time you input a value into the Generator function by the next method and print it out.

If you want to call the next method for the first time, you can input a value and wrap it 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 is not wrapped in the wrapper, it is impossible to input parameters to the next method in the first invocation.

For…of loop

The for...of loops can automatically traverse Iterator objects generated by the Generator function, and no longer need to call the next method.

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 a for...of loop that displays the values ​​of five yield expressions in turn. It should be noted here that once the done property of the returned object of the next method is true, the for...of loop will be aborted and the return object will not be included. So the returned value 6 is not included in the for...of loop.

Below is an example of using the Generator function and for...of loop to implement a 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 you can see from the code above, you don’t need to call the next method when using the for...of statement.

With for...of loop, you can write methods that traverse any object. Native JavaScript objects don’t have a traversable interface, you can’t use for...of loop, but you can add this interface with the Generator function.

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 have an Iterator interface natively and cannot be traversed with for...of. At this point, we add a traversal interface objectEntries.

Another way to add the Iterator interface is to add the Generator function to the object’s Symbol.iterator property.

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

In addition to for...of loops, extension operator (...), destructuring assignments, and Array.from methods also use the Iterator interface. They all take the Generator object returned by the Generator function as a parameter.

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

// extended operator
[...numbers()] // [1, 2]

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

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

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

Generator.prototype.throw()

The Generator object returned by the Generator function has a throw method that throws an error outside the function and then captures it inside 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 generator object i throws two consecutive errors. The first error is caught by the catch statement inside the Generator function. Since the catch statement inside the Generator function has already been executed, the second error will not be caught again, so this error is thrown by the Generator function body and captured by the catch statement outside the function .

The throw method can accept a parameter that is received by the catch statement and suggests throwing 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: error!(…)

Be careful not to confuse the throw method of the Generator object with the global throw command. The error in the above code is thrown by the throw method of the Generator object, not by the global throw command. The latter can only be captured by catch statements outside the Generator 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 above code only captured error a. Because when a error is thrown, the remaining statements in the try code block will not be executed.

If there is no try...catch code block deployed inside the Generator function, the error thrown by the throw method will be caught by the outer 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);
}
// 外部捕获 a

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

If there is no try...catch code block deployed inside and outside the Generator function, the program will report an error and directly interrupt 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 an error is thrown, no try...catch code block can catch this error, causing the program to report an error and interrupt execution.

The error thrown by the throw method is caught internally, provided that the next method has to 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 at one time. At this time, the thrown error will not be captured internally, but will be thrown directly outside, causing the program to go wrong. This behavior is actually very easy to understand, because the first execution of the next method is equivalent to starting the internal code that executes the Generator function, otherwise the Generator function has not yet started executing, and the throw method throwing error can only be thrown outside the function.

After the throw the method is captured, the next yield expression is attached. That is to say, 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 it will print b. In addition, you can also see that as long as the try...catch code function is deployed inside the Generator function, the error thrown by the throw method of the Generator object does not affect the other codes.

In addition, the global throw command and g.throw method are irrelevant, and the two do not affect 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 does not affect the state of the Generator object, so the two next methods are executed correctly.

This mechanism greatly facilitates the handling of errors. One block of  try...catch can catch multiple yield expressions. If you use the callback function, you want to capture multiple errors, you have to write an error handling statement inside each function, now only write the catch statement inside the Generator function .

Generator.prototype.return()

The Generator object, returned by the Generator function, has a return method that returns the given value and terminates 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 Generator object g calls the return method, the value property of the returned object is foo, which is the parameter of the method return. Also, the Generator function is terminated, the done property of the returned object is true. All later next methods returns objects with the done property being true.

If no argument is supplied when the return method is called, the value property of the returned 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, the return method is deferred until the finally code block is executed.

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, and then waits until the finally code block is finished, and then terminates the Generator function.

The commonality of next(), throw(), and return()

next()throw() and return(): These three methods are essentially the same thing can be put together to understand. Their role is to have the Generator function resume execution and replace the yield expression with a different statement .

next() is to replace 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}
// let result = yield x + y; ==> let result = 1;

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

throw() is to replace the yield expression with a throw statement.

gen.throw(new Error('Something goes wrong.')); // Uncaught Error: Something goes wrong.
// let result = yield x + y; ==> let result = throw(new Error('Something goes wrong.'));

return() is to replace the yield expression with a return statement.

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

Yield* expression

If you call another Generator function inside a Generator function, You need to manually complete the traversal inside the function body of the latter.

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, foo and bar both are Generator functions. Since bar is called inside foo, you need to manually traverse foo. If there are multiple Generator functions nested, it is very cumbersome to write.

ES6 provides yield* expressions as a workaround for executing 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 a comparative example.

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

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

var gen = outer1()
gen.next().value // "open"
gen.next().value // Return a Generator 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, outer2 used yield*, whilc outer1 took yieldouter1 obtains a Generator object. outer2 returns the internal string value of the inner function.

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

The following Generator functions (returnwhen there is no statement) are equivalent to deploying a for...of loop inside the Generator function .

Example 1

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

Example 2

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;
  }
}

Generator function as an object property

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

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

In the above code, the myGeneratorMethod attribute is preceded by an asterisk, indicating that this attribute is a Generator function.

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

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

Generator function this

The Generator function always returns a Generator object, and ES6 specifies that the object is an instance of the Generator function and also inherits methods from the prototype property 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 Generator object obj returned by the Generator function g is an instance of g and it inherited g.prototype. However, if you treat g as a normal constructor, it won’t take effect, because the returned object is always a Generator 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 a property a to this, but the object obj can not get this property.

The Generator function can’t be used with the new command and will report an error.

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 with the constructor F, and the result is incorrect, because it F is not a constructor.

So, is there a way to get the Generator function to return a normal object instance?

Below is a workaround. First, generate an empty object that is bound to the this pointer inside of the Generator function using the method call. Thus, after the constructor call, 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, all internal properties are bound to the objobject, so the obj object becomes an instance of F.

In the above code, we execute the Generator object f, but the generated object instance is obj, is there a way to unify these two objects?

We can change obj to 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}

f.a // 1
f.b // 2
f.c // 3

And then change F to a constructor function and you can now execute 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}

f.a // 1
f.b // 2
f.c // 3

Meaning of JavaScript Generator

Generator and state machine

Generator is the best structure for implementing state machines. For example, the clock function below 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), and each time it is run, it changes state once. If this function is implemented with Generator, it is as follows.

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

Compared with the ES5 implementation, the above Generator implementation can see that there are fewer external variables to save the state ticking, which is more concise and safer (the state will not be illegally tampered with), more in line with the idea of ​​functional programming, in writing more elegant. The reason why Generator can save state without external variables is because it already contains a state information, that is, whether it is currently in a pause state.

Generator and coroutine

A coroutine is a way of running a program and can be understood as a “cooperative thread” or a “cooperative function.” The coroutine can be implemented in single thread or in multiple threads. The former is a special subroutine, the latter is a special kind of thread.

(1) Differences between coroutines and subroutines

The traditional “subroutine” adopts a stacked “last in, first out” execution mode, and the execution of the parent function ends only when the called subfunction is completely executed. Unlike coroutines, multiple threads (in a single-thread case, i.e., multiple functions) can execute in parallel, but only one thread (or function) is in a running state, and other threads (or functions) are in a suspended state.

Execution rights can be exchanged between threads (or functions). That is to say, if one thread (or function) is executed halfway, execution can be suspended, execution rights can be handed over to another thread (or function), and execution resumes when the execution right is reclaimed later. This kind of thread (or function) that can execute and exchange execution rights in parallel is called a coroutine.

From the implementation point of view, in the memory, the subroutine only uses one stack, and the coroutine has multiple stacks at the same time, but only one stack is in the running state. That is, the coroutine occupies more memory to achieve parallelism in multitasking.

(2) Differences between coroutines and ordinary threads

It is not difficult to see that coroutines are suitable for environments with multitasking. In this sense, it is very similar to ordinary threads, which have their own execution contexts, and can share global variables. The difference is that multiple threads can be running at the same time, but there can only be one coroutine running, and other coroutines are in a pause state. In addition, the normal thread is preemptive. In the end, which thread gets the resource first, is 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, you can only maintain one call stack. After the introduction of the coroutine, each task can maintain its own call stack. The biggest benefit of doing this is that when an error is thrown, the original call stack can be found.

The Generator function is an implementation of ES6 for coroutines, but it is not fully implemented. The Generator function is called “semi-coroutine”, meaning that only the caller of the Generator function can return the execution of the program to the Generator function. In the case of a fully executed coroutine, any function can cause the suspended coroutine to continue execution.

If you use the Generator function as a coroutine, you can write multiple tasks that need to work together to be a Generator function, and use the yield expression exchange control between them .

Generator and context

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

This stack is a “last in, first out” data structure. The resulting context is first executed, exiting the stack, and then executing the underlying context until all code execution is complete and the stack is emptied.

This is not the case with the Generator function. It executes the resulting context. Once the yield command is encountered , it will temporarily exit the stack, but it will not disappear. All variables and objects inside will be frozen in the current state. When the next command is executed on it, the context is re-joined to the call stack, and the frozen variables and objects are resumed.

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

let g = gen();

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

In the first execution of g.next(), the context of the Generator function gen will be added to the stack, that is, the internal code of gen will start running. When encountered yield 1, the gen context exits the stack and the internal state freezes. On the second execution of g.next(), the gen context is re-joined to the stack, becomes the active context, and resumes execution.

Closing Words

This article talked about JavaScript generator, which is one of my favourite features in ES6. I hope you can follow this tutorial. If not, please leave me a comment.

Also, I recommend the MDN article for Iterators and Generators.

Some other useful articles:

Don’t stop learning!

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *