Please indicate the source of reprint:
Generator function syntax parsing – Nuggets column
Generator function syntax parsing – Zhihu column
Generator syntax parsing – Blog garden
Generator functions are an asynchronous programming solution provided by ES6 with a completely different syntax from traditional functions. The following describes the Generator functions.
In fact, the purpose of writing this article is very simple. I want to sort out my understanding of Generator and reserve some knowledge for learning async functions.
The Generator function
- The basic concept
- Yield expression
- Next method
- Parameters to the next method
- Yield expression *
- Relationship to the Iterator interface
- for… Of circulation
- A Generator function as an object property
- This in the Generator function
- application
The basic concept
A Generator function (also called a Generator function) can be understood in four ways:
Form: A Generator function is a normal function with two additional features. First, there is an extra ‘*’ between the function keyword and the function. Second, the function uses yield expressions internally to define each state in the Generator function.
Syntactically: Generator functions encapsulate multiple internal states (defined by yield expressions). An Iterator object is returned when the Generator is executed. That is, the Generator is an iterator object Generator function that encapsulates multiple states internally. From the returned Iterator, each internal state of the Generator function can be iterated (calling the next method) in turn.
On call: Normal functions execute immediately after being called, whereas Generator functions do not execute immediately after being called and instead return an Iterator object. Next method to traverse through the Iterator object within each state of the definition of yield expression.
It seems that the * can be placed anywhere. I prefer the first one
function *gen() {})function* gen () {}
function * gen () {}
function*gen () {}
Copy the code
Yield expression
Yield means to produce or yield, so the yield expression also serves two purposes: to define internal state and to pause execution.
Give me a chestnut:)
function *gen () {
yield 1
yield 2
return3} const g = gen() // Iterator g.ext () // {value: 1,done: false}
g.next() // {value: 2, done: false}
g.next() // {value: 3, done: true}
Copy the code
As you can see from the code above, the Gen function defines two internal states using yield expressions. It can also be seen that a return statement can have only one, whereas a yield expression can have multiple.
Instead of executing gen immediately, a traverser object is returned. If you want to get every state defined by the yield expression, you call the next method.
Each call to the Next method returns an object containing both value and done attributes, stopping at the end of some yield expression. The value property value is the yield expression value; The done attribute is a Boolean value indicating whether the traversal is complete.
In addition, the yield expression returns no value, or the return value is undefined. We’ll explain how to pass a return value to a yield expression later.
Note that the value of the yield expression is only available when the next method is called. Thus providing JavaScript with manual ‘Lazy Evaluation’ capabilities.
Typically, Generator functions are used in conjunction with yield expressions that define multiple internal states. However, if a function that does not use a yield expression is simply a deferred function, it makes little sense to me…
function *gen () {
console.log('case'} window.setTimeout(() => {gen().next()}, 2000)} window.setTimeout(() => {gen().next()}, 2000) // So Generator functions work better with yield expressionsCopy the code
Also, if the yield expression is used in another expression, it needs to be enclosed in parentheses. Function arguments and statements can be used without parentheses.
function *gen () {
console.log('hello'+ yield) by the console. The log ('hello'The console + (yield))). The log ('hello' + yield 'case') by the console. The log ('hello' + (yield 'case')) √ foo(yield 1) √ const param = yield 2 √}Copy the code
Next method
The yield expression pauses execution, whereas the next method resumes execution. Each time the next method is called, execution begins at the head of the function or where it was last stopped until the next yield expression (return statement) is encountered. At the same time, when the next method is called, an object containing both value and done attributes is returned. The value of the value can be the yield expression, the value after the return statement, or undefined. The done attribute indicates whether the traversal is complete.
The next method of the traverser object (inherited from the Generator function) runs as follows
- When a yield expression is encountered, subsequent operations are paused and the value of the expression immediately following the yield expression is used as the value of the returned object’s value property.
- The next time the next method is called, execution continues until the next yield expression is encountered.
- If no new yield expression is encountered, it runs until the end of the function until a return statement is encountered, using the value of the expression following the return statement as the value of the returned object’s value property.
- If the function has no return statement, the value property of the returned object is undefined.
As you can see from the above run logic, the value of the returned object property has three results:
- The value following the yield expression
- The value after the return statement
- undefined
That is, if there is a yield expression, then the value of the value attribute is the yield expression; If there is no yield expression, the value property value is equal to the value following the return statement; If neither yield expression nor return statement exists, the value property value is undefined. Here’s an example:
function *gen () {
yield 1
yield 2
return 3
}
const g = gen()
g.next() // {value: 1, done: false}
g.next() // {value: 2, done: false}
g.next() // {value: 3, done: true}
g.next() // {value: undefined, done: true}
Copy the code
This example is easy to follow from the next run logic. Call the gen function to return the traverser object.
The first time the next method is called, execution stops when the first yield expression is encountered. The value attribute is 1, the value after the yield expression. Done is false to indicate that the traversal is not finished.
When the next method is called the second time, execution begins after the yield expression is paused until the next yield expression is encountered. The value attribute is 2 and done is false.
When the next method is called the third time, the execution begins after the last paused yield expression. Since there are no yield expressions, the function ends when a return statement is encountered, and the value of the value of the return statement is the value of the done attribute, and the value of the done attribute is true, indicating that the traversal is complete.
When next is called the fourth time, the value property is undefined, and the done property is true to indicate that the traversal is complete. The next method will be called with these two values.
Parameters to the next method
The yield expression itself returns no value, or always returns undefined.
function *gen () {
var x = yield 'hello world'
var y = x / 2
return [x, y]
}
const g = gen()
g.next() // {value: 'hello world'.done: false}
g.next() // {value: [undefined, NaN], done: true}
Copy the code
As you can see from the above code, on the first call to the next method, the value property is ‘hello world’. On the second call, since the value of y depends on x, and the yield expression returns no value, undefined is returned to x. In this case, undefined / 2 is NaN.
To solve the above problem, pass parameters to the next method. The next method can take an argument that is treated as the return value of the previous yield expression.
function *gen () {
var x = yield 'hello world'
var y = x / 2
return [x, y]
}
const g = gen()
g.next() // {value: 'hello world'.done: false}
g.next(10) // {value: [10, 5], done: true}
Copy the code
When 10 is passed to the second next method, the yield expression returns 10, that is, var x = 10, so the variable y is 5.
Note that because the argument to the next method represents the return value of the previous yield expression, passing the argument is invalid the first time the next method is used. The V8 engine ignores arguments to the next method the first time it is used, only from the second time it is used. Semantically, the first next method is used to start the traverser object, so it takes no arguments. As a result, the next method is used one more time than the yield expression.
If you want to pass parameters the first time you call the next method, you can use closures.
// The next method is actually executed inside the closurefunction wrapper (gen) {
return function(... args) { const genObj = gen(... args) genObj.next()return genObj
}
}
const generator = wrapper(function *generator () {
console.log(`hello ${yield}`)
return 'done'
})
const a = generator().next('keith')
console.log(a) // hello keith, done
Copy the code
In effect, the **yield expression and the next method form two-way information passing. ** Yield expressions can pass values outward, while next method parameters can pass values inward.
Yield expression *
If you call another Generator function within a Generator function, this is invalid by default.
function *foo () {
yield 1
}
function *gen () {
foo()
yield 2
}
const g = gen()
g.next() // {value: 2, done: false}
g.next() // {value: undefined, done: true}
Copy the code
As you can see from the code above, execution is not stopped at yield 1. This is where the yield* expression is needed. Syntactically, if a yield expression is followed by an traverser object, you need to add an asterisk after the yield expression to indicate that it returns an traverser object. In fact, the yield* expression is for… Short for the loop, which could have been used for… The of loop replaces the yield* expression
function *foo () {
yield 1
}
function *gen () {
yield* foo()
yield 2
}
const g = gen()
g.next() // {value: 1, done: false}
g.next() // {value: 2, done: false}
g.next() // {value: undefined, done: true} // equivalent tofunction *gen() {yield 1 yield 2} //function *gen () {
for (let item of foo()) {
yield item
}
yield 2
}
Copy the code
If yield foo() is used directly, the value property of the returned object is a traverser object. Not the internal state of the Generator function.
function *foo () {
yield 1
}
function *gen () {
yield foo()
yield 2
}
const g = gen()
g.next() // {value: Generator, done: false}
g.next() // {value: 2, done: false}
g.next() // {value: undefined, done: true}
Copy the code
In addition, any data type (Array, String) that has an Iterator interface can be yield* traversed
const arr = ['a'.'b']
const str = 'keith'
function *gen () {
yield arr
yield* arr
yield str
yield* str
}
const g = gen()
g.next() // {value: ['a'.'b'].done: false}
g.next() // {value: 'a'.done: false}
g.next() // {value: 'b'.done: false}
g.next() // {value: 'keith'.done: false}
g.next() // {value: 'k'.done: false}...Copy the code
If a return statement exists in a Generator function, use let value = yield* iterator to obtain the return value.
function *foo () {
yield 1
return2}function *gen () {
var x = yield* foo()
return x
}
const g = gen()
g.next() // {value: 1, done: false}
g.next() // {value: 2, done: true}
Copy the code
Yield * expressions make it easy to retrieve members of nested arrays.
// Const arr = [1, [[2, 3], 4]] const STR = arr.toString().replace(/,/g,)' ')
for (letItem of STR) {console.log(+item) // 1, 2, 3, 4} // Use yield* expressionfunction *gen (arr) {
if (Array.isArray(arr)) {
for (let i = 0; i < arr.length; i++) {
yield * gen(arr[i])
}
} else {
yield arr
}
}
const g = gen([1, [[2, 3], 4]])
for (let item of g) {
console.log(item) // 1, 2, 3, 4
}
Copy the code
Relationship to the Iterator interface
The symbol. iterator property of any object that points to the default traverser object generator. The Generator function is also an iterator object Generator, so you can assign the Generator function to the symbol. iterator property, thus giving the object an Iterator interface. By default, objects do not have an Iterator interface. Objects that have an Iterator interface can be extended by operators (…). , destruct assignment, array. from and for… The of loop is iterating.
const person = {
name: 'keith',
height: 180
}
function *gen () {
const arr = Object.keys(this)
for (let item of arr) {
yield [item, this[item]]
}
}
person[Symbol.iterator] = gen
for (let [key, value] of person) {
console.log(key, value) // name keith , height 180
}
Copy the code
Generator functions After the function executes, the traverser object is returned. The object itself has the symbol. iterator property, which returns itself after execution
function *gen () {}
const g = gen()
g[Symbol.iterator]() === g // true
Copy the code
for… Of circulation
for… The of loop automatically iterates over an Iterator generated by a Generator function without calling the next method.
function *gen () {
yield 1
yield 2
yield 3
return4}for (let item of gen()) {
console.log(item) // 1 2 3
}
Copy the code
The code above uses for… The of loop displays the values of the three yield expressions in turn. Note here that once the next method returns an object with the done attribute true, for… The of loop terminates and does not contain the return object, so the 6 returned by the return statement above is not included in the for… In the loop of.
A Generator function as an object property
If an object has a Generator, you can use the shorthand
let obj = {
* gen() {}} // can also be written completelylet obj = {
gen: function *gen() {}}Copy the code
Of course, if you’re in a constructor, the shorthand is the same.
class F {
* gen() {}}Copy the code
This in the Generator function
The this object in the Generator is similar to the this object in the constructor. Let’s start by looking at how the new keyword in the constructor works.
function F () {
this.a = 1
}
const f = new F()
Copy the code
- Call the constructor F to return the instance object F
- Point this inside the constructor to the instance object
- Assigns the stereotype object in the constructor to the stereotype of the instance object
- Execute the code in the constructor
Calling the Generator returns the traverser object, not the instance object, and therefore does not get the private properties and methods on the instance object to which this points. However, the traverser object can inherit properties and methods (public properties and methods) from the Generator function’s Prototype object.
function *Gen () {
yield this.a = 1
}
Gen.prototype.say = function () {
console.log('keith')
}
const g = new Gen()
g.a // undefined
g.say() // 'keith'
Copy the code
If you want to fix this directivity, you can use the Call method to bind the scope in which the function executes to a generator. prototype prototype object. Doing so makes private properties and methods public because they are on the prototype object.
function *Gen () {
this.a = 1
yield this.b = 2
yield this.c = 3
}
const g = Gen.call(Gen.prototype)
g.next() // {value: 2, done: false}
g.next() // {value: 3, done: false}
g.next() // {value: undefined, done: true} prototype g.b // 2, g.c // 3, ibidCopy the code
application
The use of Generator functions is primarily for asynchronous programming and will be shared in the next article. Please look forward to:)