复习建议看此,非表面的讲解
简介
一般js开发者对函数都有一个几乎共识:一旦函数开始执行,它将运行直至完成,没有其他的代码可以在运行期间干扰它;但是ES6引入了一种新型的函数,它不按照“运行至完成”的行为进行动作。这种新型的函数称为“generator(生成器)”。
Generator 函数是一个普通函数,但是有两个特征。一是,function
关键字与函数名之间有一个星号;二是,函数体内部使用yield
表达式,定义不同的内部状态(yield
在英语里的意思就是“产出”)。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next(); -> hello
hw.next(); -> world
hw.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