7. 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;
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;
8. yield* 表达式
如果你打算在Generater函数内部,调用另一个Generator函数,默认情况下是没有效果的。
function* foo() { yield 'a'; yield 'b'; } function* bar() { yield 'x'; foo(); yield 'y'; } for (let v of bar()){ console.log(v); } // "x" // "y"
可见,并没有遍历出’a’和’b’。那么如果想在一个 Generator 函数里调用另一个 Generator 函数,怎么办?
用 yield* 语句。
function* bar() { yield 'x'; yield* foo(); yield 'y'; } // 上个函数等同于 function* bar() { yield 'x'; yield 'a'; yield 'b'; yield 'y'; } // 也等同于 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"
也就是说,我们约定被调用的 Generator 函数为 A 函数,调用A函数的 Generator 函数为 B 函数。yield* 语句的作用,就是遍历一遍 A 函数的迭代器对象。A函数(没有 return 语句时)是 for…of 的一种简写形式,完全可以用 for…of 替代 yield*。反之,由于B 函数的 return 语句,不会被 yield* 遍历,所以需要用 var value = yield* iterator 的形式获取 return 语句的值。
function *foo() { yield 2; yield 3; return "foo"; } function *bar() { yield 1; var v = yield *foo(); console.log( "v: " + v ); yield 4; } var it = bar(); it.next() // {value: 1, done: false} it.next() // {value: 2, done: false} it.next() // {value: 3, done: false} it.next(); // "v: foo" // {value: 4, done: false} it.next() // {value: undefined, done: true}
上面代码在第四次调用 next 方法的时候,屏幕上会有输出,这是因为函数 foo 的 return 语句,向函数 bar 提供了返回值。
如果 yield* 后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员。
function* gen(){ yield* ["a", "b", "c"]; } gen().next() // { value:"a", done:false }
上面代码中,yield 命令后面如果不加星号,返回的是整个数组,加了星号就表示返回的是数组的遍历器对象。
实际上,任何数据结构只要有 Iterator 接口,就可以被 yield* 遍历。
let read = (function* () { yield 'hello'; yield* 'hello'; })(); read.next().value // "hello" read.next().value // "h"
上面代码中,yield 表达式返回整个字符串,yield* 语句返回单个字符。因为字符串具有 Iterator 接口,所以被yield*遍历。
9. 作为对象属性的 Generator 函数
如果一个对象的属性是 Generator 函数,可以简写成下面的形式。
myGeneratorMethod 属性前面有一个星号,表示这个属性是一个 Generator 函数。
let obj = { * myGeneratorMethod() { ··· } };
它的完整形式如下,与上面的写法是等价的。
let obj = { myGeneratorMethod: function* () { // ··· } };
10. Generator 函数的 this
Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的 prototype 对象上的方法。
function* g() {} g.prototype.hello = function () { return 'hi!'; }; let obj = g(); obj instanceof g // true obj.hello() // 'hi!'
上面代码表明,Generator 函数 g 返回的遍历器 obj,是 g 的实例,而且继承了g.prototype。但是,如果把g当作普通的构造函数,并不会生效,因为g返回的总是遍历器对象,而不是this对象。
function* g() { this.a = 11; } let obj = g(); obj.next(); obj.a // undefined
上面代码中,Generator 函数 g 在 this 对象上面添加了一个属性a,但是 obj 对象拿不到这个属性。
Generator 函数也不能跟 new 命令一起用,会报错。
那么,有没有办法让 Generator 函数返回一个正常的对象实例,既可以用 next 方法,又可以获得正常的 this?
下面是一个变通方法。首先,生成一个空对象,使用 call 方法绑定 Generator 函数内部的this。这样,构造函数调用以后,这个空对象就是 Generator 函数的实例对象了。
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
上面代码中,首先是 F 内部的 this 对象绑定 obj 对象,然后调用它,返回一个 Iterator 对象。这个对象执行三次 next 方法(因为F 内部有两个 yield 表达式),完成 F 内部所有代码的运行。这时,所有内部属性都绑定在 obj 对象上了,因此 obj 对象也就成了F 的实例。
上面代码中,执行的是遍历器对象 f,但是生成的对象实例是 obj,有没有办法将这两个对象统一呢?
一个办法就是将 obj 换成 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
再将 F 改成构造函数,就可以对它执行 new 命令了。
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
11. 含义
11.1 Generator 与状态机
Generator 是实现状态机的最佳结构。比如,下面的 clock 函数就是一个状态机。
var ticking = true; var clock = function() { if (ticking) console.log('Tick!'); else console.log('Tock!'); ticking = !ticking; }
上面代码的 clock 函数一共有两种状态( Tick 和 Tock ),每运行一次,就改变一次状态。
这个函数如果用 Generator 实现,就是下面这样。
var clock = function* () { while (true) { console.log('Tick!'); yield; console.log('Tock!'); yield; } };
上面的 Generator 实现与 ES5 实现对比,可以看到少了用来保存状态的外部变量ticking,这样就更简洁,更安全(状态不会被非法篡改)、更符合函数式编程的思想,在写法上也更优雅。Generator 之所以可以不用外部变量保存状态,是因为它本身就包含了一个状态信息,即目前是否处于暂停态。
11.2 Generator 与上下文
JavaScript 代码运行时,会产生一个全局的上下文环境(context,又称运行环境),包含了当前所有的变量和对象。然后,执行函数(或块级代码)的时候,又会在当前上下文环境的上层,产生一个函数运行的上下文,变成当前(active)的上下文,由此形成一个上下文环境的堆栈(context stack)。
这个堆栈是“后进先出”的数据结构,最后产生的上下文环境首先执行完成,退出堆栈,然后再执行完成它下层的上下文,直至所有代码执行完成,堆栈清空。
Generator 函数不是这样,它执行产生的上下文环境,一旦遇到 yield 命令,就会暂时退出堆栈,但是并不消失,里面的所有变量和对象会冻结在当前状态。等到对它执行 next 命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行。
function* gen() { yield 1; return 2; } let g = gen(); console.log( g.next().value, g.next().value, );
上面代码中,第一次执行 g.next() 时,Generator 函数 gen 的上下文会加入堆栈,即开始运行gen内部的代码。等遇到 yield 1时,gen 上下文退出堆栈,内部状态冻结。第二次执行 g.next() 时,gen 上下文重新加入堆栈,变成当前的上下文,重新恢复执行。
12. 应用
12.1 部署 Iterator 接口
利用 Generator 函数,可以在任意对象上部署 Iterator 接口。
function* iterEntries(obj) { let keys = Object.keys(obj); for (let i=0; i < keys.length; i++) { let key = keys[i]; yield [key, obj[key]]; } } let myObj = { foo: 3, bar: 7 }; for (let [key, value] of iterEntries(myObj)) { console.log(key, value); } // foo 3 // bar 7
12.2 异步操作的同步化表达
Generator 函数的暂停执行的效果,意味着可以把异步操作写在 yield 表达式里面,等到调用 next 方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在 yield 表达式下面,反正要等到调用 next 方法时再执行。所以,Generator 函数的一个重要实际意义就是用来处理异步操作,改写回调函数。
function* loadUI() { showLoadingScreen(); yield loadUIDataAsynchronously(); hideLoadingScreen(); } var loader = loadUI(); // 加载UI loader.next() // 卸载UI loader.next()
上面代码中,第一次调用 loadUI 函数时,该函数不会执行,仅返回一个遍历器。下一次对该遍历器调用 next 方法,则会显示Loading 界面(showLoadingScreen),并且异步加载数据(loadUIDataAsynchronously)。等到数据加载完成,再一次使用 next 方法,则会隐藏 Loading 界面。可以看到,这种写法的好处是所有 Loading 界面的逻辑,都被封装在一个函数,按部就班非常清晰。
Ajax 是典型的异步操作,通过 Generator 函数部署 Ajax 操作,可以用同步的方式表达。
function* main() { var result = yield request("http://some.url"); var resp = JSON.parse(result); console.log(resp.value); } function request(url) { makeAjaxCall(url, function(response){ it.next(response); }); } var it = main(); it.next();
上面代码的 main 函数,就是通过 Ajax 操作获取数据。可以看到,除了多了一个 yield,它几乎与同步操作的写法完全一样。注意,makeAjaxCall 函数中的next方法,必须加上 response 参数,因为 yield 表达式,本身是没有值的,总是等于 undefined。
12.3 控制流管理
如果有一个多步操作非常耗时,采用回调函数,可能会写成下面这样。
step1(function (value1) { step2(value1, function(value2) { step3(value2, function(value3) { step4(value3, function(value4) { // Do something with value4 }); }); }); });
采用 Promise 改写上面的代码
Promise.resolve(step1) .then(step2) .function* longRunningTask(value1) { try { var value2 = yield step1(value1); var value3 = yield step2(value2); var value4 = yield step3(value3); var value5 = yield step4(value4); // Do something with value4 } catch (e) { // Handle any error from step1 through step4 } } then(step3) .then(step4) .then(function (value4) { // Do something with value4 }, function (error) { // Handle any error from step1 through step4 }) .done();
上面代码已经把回调函数,改成了直线执行的形式,但是加入了大量 Promise 的语法。Generator 函数可以进一步改善代码运行流程。
function* longRunningTask(value1) { try { var value2 = yield step1(value1); var value3 = yield step2(value2); var value4 = yield step3(value3); var value5 = yield step4(value4); // Do something with value4 } catch (e) { // Handle any error from step1 through step4 } }
利用 for…of 循环会自动依次执行 yield 命令的特性,提供一种更一般的控制流管理的方法。
let steps = [step1Func, step2Func, step3Func]; function* iterateSteps(steps){ for (var i=0; i< steps.length; i++){ var step = steps[i]; yield step(); } }
上面代码中,数组 steps 封装了一个任务的多个步骤,Generator 函数 iterateSteps 则是依次为这些步骤加上 yield 命令。
将任务分解成步骤之后,还可以将项目分解成多个依次执行的任务。
let jobs = [job1, job2, job3]; function* iterateJobs(jobs){ for (var i=0; i< jobs.length; i++){ var job = jobs[i]; yield* iterateSteps(job.steps); } }
上面代码中,数组 jobs 封装了一个项目的多个任务,Generator 函数 iterateJobs 则是依次为这些任务加上 yield* 命令。
最后,就可以用 for…of 循环一次性依次执行所有任务的所有步骤。
for (var step of iterateJobs(jobs)){ console.log(step.id); }
再次提醒,上面的做法只能用于所有步骤都是同步操作的情况,不能有异步操作的步骤。
for…of 的本质是一个 while 循环,所以上面的代码实质上执行的是下面的逻辑。
var it = iterateJobs(jobs); var res = it.next(); while (!res.done){ var result = res.value; // ... res = it.next(); }
12.4 斐波那契数列
斐波那契数列是什么?它指的是这样一个数列 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144…
这个数列前两项是0和1,从第3项开始,每一项都等于前两项之和。
function* flb(){ let [pre, cur] = [0, 1] for(;;) { [pre, cur] = [cur, pre + cur] yield cur } } for(let k of flb()){ if( k > 1000 ) break console.log(k) } // 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987