复习建议看此,非表面的讲解
简介
一般js开发者对函数都有一个几乎共识:一旦函数开始执行,它将运行直至完成,没有其他的代码可以在运行期间干扰它;但是ES6引入了一种新型的函数,它不按照“运行至完成”的行为进行动作。这种新型的函数称为“generator(生成器)”。
Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。
function* helloWorldGenerator() {yield 'hello';yield 'world';return 'ending';}var hw = helloWorldGenerator();hw.next(); -> hellohw.next(); -> worldhw.next(); -> ending//调用遍历器对象的next方法,使得指针移向下一个状态。//也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,//直到遇到下一个yield表达式(或return语句)为止。//换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行
调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象(iterator),通过next方法来控制
所以,记得总是用一个无参数的next()来启动generator。
yield
遍历器对象的next方法的运行逻辑如下(遇到yield表达式就暂停,先执行yield后面的操作,直到遇到return结束为止)
- 遇到
yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。 - 下一次调用
next方法时,再继续往下执行,直到遇到下一个yield表达式。 - 如果没有再遇到新的
yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。 - 如果该函数没有
return语句,则返回的对象的value属性值为undefined
需要注意的点
yield表达式后面的操作 只有等到调用next方法时才会被触发执行yield表达式也只能用在generator函数里面(也就是 function* () 函数),其他会报错yield表达式如果用在另一个表达式之中,必须放在圆括号里面console.log('Hello' + yield 123); // SyntaxError console.log('Hello' + (yield)); // OK
next 方法的参数
yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
function* f() {
for(var i = 0; true; i++) {
var reset = yield i; //reset为yield的返回值,为上次next调用的参数
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 }
next()&throw()&return()
next()、throw()、return()这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。
next()是将yield表达式替换成一个值。
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;
上面代码中,第二个next(1)方法就相当于将yield表达式替换成一个值1。如果next方法没有参数,就相当于替换成undefined。
throw()是将yield表达式替换成一个throw语句。
gen.throw(new Error('出错了')); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));
return()是将yield表达式替换成一个return语句。
gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;
yield* 表达式
yield*表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数。
yield委托(也就是 yield* 表达式)的目的很大程度上是为了代码组织,而且这种方式是与普通函数调用对称的。
具体可以参见 generator 委托
含义
Generator 与状态机
clock函数就是一个状态机。
var ticking = true;
var clock = function() {
if (ticking)
console.log('Tick!');
else
console.log('Tock!');
ticking = !ticking;
}
用 Generator 实现,就是下面这样。
var clock = function* () {
while (true) {
console.log('Tick!');
yield;
console.log('Tock!');
yield;
}
};
上面的 Generator 实现与 ES5 实现对比,可以看到少了用来保存状态的外部变量ticking,这样就更简洁,更安全(状态不会被非法篡改)、更符合函数式编程的思想,在写法上也更优雅。Generator 之所以可以不用外部变量保存状态,是因为它本身就包含了一个状态信息,即目前是否处于暂停态
Generator 与上下文
JavaScript 代码运行时,会产生一个全局的上下文环境(context,又称运行环境),包含了当前所有的变量和对象。然后,执行函数(或块级代码)的时候,又会在当前上下文环境的上层,产生一个函数运行的上下文,变成当前(active)的上下文,由此形成一个上下文环境的堆栈(context stack)。
这个堆栈是“后进先出”的数据结构,最后产生的上下文环境首先执行完成,退出堆栈,然后再执行完成它下层的上下文,直至所有代码执行完成,堆栈清空。
Generator 函数不是这样,它执行产生的上下文环境,一旦遇到yield命令,就会暂时退出堆栈,但是并不消失,里面的所有变量和对象会冻结在当前状态。等到对它执行next命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行
Thunks
在一般的计算机科学中,有一种老旧的前JS时代的概念,称为“thunk”。我们不在这里赘述它的历史,一个狭隘的表达是,thunk是一个JS函数——没有任何参数——它连接并调用另一个函数。
换句话讲,你用一个函数定义包装函数调用——带着它需要的所有参数——来 推迟 这个调用的执行,而这个包装用的函数就是thunk。当你稍后执行thunk时,你最终会调用那个原始的函数
function foo(x,y) {
return x + y;
}
function fooThunk() {
return foo( 3, 4 );
}
// 稍后
console.log( fooThunk() ); // 7
