一,Generator(生成器) 函数的理解
-
Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。
-
语法上,可以把理解成,Generator 函数是一个状态机,封装了多个内部状态。形式上,Generator 函数是一个普通函数。整个Generator函数就是一个封装的异步任务,或者说是异步任务的容器,异步操作需要暂停的地方,都用yield语句。
-
写法上: *号放在哪里好像都可以也。
function *gen () {}
function* gen () {}
function * gen () {}
function*gen () {}
-
Generator函数特征:
-
(1) function 关键字和函数之间有一个星号(*),且内部使用yield表达式,定义不同的内部状态(可以有多个yield)。
-
(2) 调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象(也就是一个遍历器对象(Iterator Object))。
-
(3) 依次调用遍历器对象的next方法,遍历 Generator函数内部的每一个状态。
-
(4) 每调用一次next方法都会返回一个包含value和done属性的对象,此时会停留在某个yield表达式结尾处。value属性值即是yield表达式的值;done属性是布尔值,表示是否遍历完毕。
function* fn(){ // 定义一个Generator函数 yield 'hello'; yield 'world'; return 'end'; } var f1 =fn(); // 调用Generator函数,函数并没有执行,返回的是一个Iterator对象 console.log(f1); // fn {[[GeneratorStatus]]: "suspended"} console.log(f1.next()); // {value: "hello", done: false} console.log(f1.next()); // {value: "world", done: false} console.log(f1.next()); // {value: "end", done: true} console.log(f1.next()); // {value: undefined, done: true} console.log(f1.next()); // {value: undefined, done: true}
上面的代码中可以看到传统函数和Generator函数的运行是完全不同的,传统函数调用后立即执行并输出了返回值;Generator函数则没有执行而是返回一个Iterator对象,并通过调用Iterator对象的next方法来遍历,使得指针移向下一个状态。即:每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。函数体内的执行看起来更像是“被人踢一脚才动一下”的感觉.
-
-
Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。
-
Generator 函数 返回的 Iterator对象 可以 一直向下遍历,通过判断 done 的值来确定是否 遍历结束。
二, yield 表达式
-
遍历器对象的next方法的运行逻辑如下。
(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
(4)如果该函数没有return语句,则返回的对象的value属性值为undefined。
需要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针向下移动指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
从上面的运行逻辑可以看出,返回的对象的value属性值有三种结果:
(1)yield表达式后面的值 (2)return语句后面的值 (3)undefined
yiled 和 return 的区别和联系: (1) return 只能有一个或者没有, yiled 可以有多个; (2) 每次遇到 yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。 (3) 两个都可以返回紧跟在后面的表达式值。
- yeild 表达式只能用在 Generator函数 函数中,用在其它地方都会报错
(function (){
yeild 1;
})()
// 在一个普通函数中使用yield表达式,结果产生一个句法错误
// Uncaught SyntaxError: Unexpected number
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;
}
});
};
//Uncaught SyntaxError: Unexpected identifier
//上面代码也会产生句法错误,因为forEach方法的参数是一个普通函数,但是在里面使用了yield表达式(这个函数里面还使用了yield*表达式。一种修改方法是改用for循环。
- yield表达式如果用在另一个表达式之中,必须放在圆括号里面。作为函数参数和语句是可以不使用圆括号。
function *gen () {
console.log('hello' + yield) ×
console.log('hello' + (yield)) √
console.log('hello' + yield '凯斯') ×
console.log('hello' + (yield '凯斯')) √
foo(yield 1) √
const param = yield 2 √
}
function *gen () {
console.log('hello' + (yield))
console.log('hello' + (yield '凯斯'))
foo(yield 1)
const param = yield 2
}
function foo(x) {
console.log("value:"+x)
}
let ms = gen();
console.log(ms.next()) //{ value: undefined, done: false }
console.log(ms.next()) // helloundefined { value: '凯斯', done: false }
console.log(ms.next()) //helloundefined { value: 1, done: false }
console.log(ms.next()) //value:undefined { value: 2, done: false }
三,next方法
-
每一次调用next方法,就会从函数头部或者上一次停下来的地方开始执行,直到遇到下一个yield表达式(return 语句)为止。同时,调用next方法时,会返回包含value和done属性的对象,value属性值可以为yield表达式、return语句后面的值或者undefined值,done属性表示遍历是否结束。
-
由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。
-
yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
-
next方法的参数
(1) next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。相当于把 yield 表达式替换为参数值,
(2) Generator 函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过next方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
function *gen(x){ let y = 2 + (yield (x*2)); let m = yield (y + 1); return y + m + x; } let a = gen(1); console.log(a.next()) // { value: 2, done: false } console.log(a.next()) // { value: NaN, done: false } console.log(a.next()) // { value: NaN, done: true } /* 上面代码中,第一次运行next方法的时候不带参数,执行yield (x*2),value:2;第二次运行next方法的时候不带参数,导致 y 的值等于2 + undefined(即NaN),y+1 后还是NaN,因此返回对象的value属性也等于NaN。第三次运行Next方法的时候不带参数,所以 m 等于undefined,返回对象的value属性等于 1 + NaN + undefined,即NaN。 */ let g = gen(1); console.log(g.next()) // { value: 2, done: false } console.log(g.next(2)) // { value: 5, done: false } console.log(g.next(5)) // { value:10 , done: true } /* 上面代码第一次调用g的next方法时,返回x*2的值2; 第二次调用next方法,将上一次yield表达式的值设为2,因此y等于4,返回y+1的值5;第三次调用next方法,将上一次yield表达式的值设为5,因此m等于5,这时x等于1,y等于4,所以return语句的值等于10。 */
注意,由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。
四,for…of循环
- for…of循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法。
function *gen () {
yield 1
yield 2
yield 3
return 4
}
for (let item of gen()) {
console.log(item) // 1 2 3
}
上面代码使用for…of循环,依次显示 3 个yield表达式的值。这里需要注意,一旦next方法的返回对象的done属性为true,for…of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的4,不包括在for…of循环之中。
- 除了for…of循环以外,扩展运算符(…)、解构赋值和Array.from方法内部调用的,都是遍历器接口。这意味着,它们都可以将 Generator 函数返回的 Iterator 对象,作为参数。
function *gen () {
yield 1
yield 2
yield 3
return 4
}
console.log([...gen()]); // 1 2 3
console.log(Array.from(gen())); // 1 2 3
let [x,y,z] = gen();
console.log([x,y,z]); // 1 2 3
五,yiled* 表达式
如果在 Generator 函数内部,调用另一个 Generator 函数。需要在前者的函数体内部,手动完成遍历。
function *gen() {
yield 1
yield 2
yield 3
return 4
}
function *foo(){
yield "a";
for(let i of gen()){
console.log(i);
}
return 0;
}
let m = foo();
console.log(m.next()) // { value: 'a', done: false }
console.log(m.next()) //1 2 3 { value: 0, done: true }
// 以上代码中可以看出来,如果有多个 Generator 函数嵌套,写起来就非常麻烦。
yield*表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数。
从语法角度看,如果yield表达式后面跟的是一个遍历器对象,需要在yield表达式后面加上星号,表明它返回的是一个遍历器对象。这被称为 yield* 表达式。
function *gen() {
yield 1
yield 2
yield 3
return 4
}
function *foo(){
yield "a";
yield* gen();
return 0;
}
let m = foo();
console.log(m.next()) //{ value: 'a', done: false }
console.log(m.next()) //{ value: 1, done: false }
console.log(m.next()) //{ value: 2, done: false }
console.log(m.next()) //{ value: 3, done: false }
console.log(m.next()) //{ value: 0, done: true }