Generator函数(下)
yield*语句
如果在Generator函数
内部调用另一个Generator函数
,默认情况下是没有效果的。
js
function* foo () {
yield 'a';
yield 'b';
}
function* bar () {
yield 'x';
foo();
yield 'y';
}
for (let v of bar()) {
console.log(v);
}
// 'x'
// 'y'
上面的代码中,foo
和bar
都是Generator函数
,在bar
里面调用foo
是不会有效果的。
这时就需要用到yield*语句
,用来在一个Generator函数
里面执行另一个Generator函数
。
js
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'
}
从语法角度看,如果yield命令
后面跟的是一个遍历器对象,则需要在yield命令
后面加上星号
,表明返回的是一个遍历器对象。这被称为yield*语句
。
yield*语句
等同于在Generator函数
内部部署一个for...of循环
。
js
function* concat(iter1, iter2) {
yield* iter1;
yield* iter2;
}
// 等同于
function* concat(iter1, iter2) {
for (var value of iter1) {
yield value;
}
for (var value of iter2) {
yield value;
}
}
上面的代码表明,yield*
不过是for...of
的一种简写形式,完全可以由后者替代。
如果yield*
后面跟着一个数组,由于数组原生支持遍历器,因此会遍历数组成员。
js
function* gen() {
yield* ['a', 'b', 'c'];
}
gen().next() // {value: 'a', done: false}
上面的代码中,yield命令
后面如果不加星号,返回的是整个数组,加了星号就表示返回数组的遍历器对象。
实际上,任何数据结构只要有Iterator接口
,就可以用yield*
遍历。
js
let read = (function* () {
yield 'hello';
yield* 'hello';
})();
read().next().value // 'hello'
read().next().value // 'h'
如果被代理的Generator函数
有return语句
,那么可以向代理它的Generator函数
返回数据。
js
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}
在上面的代码中,第4次调用next方法
时,屏幕上会有输出,这是因为函数foo的return语句
向函数bar
提供了返回值。
再看一个例子:
js
function* genFuncWithReturn () {
yield 'a';
yield 'b';
return 'The result';
}
function* logReturned (genObj) {
let result = yield* genObj;
console.log(result);
}
[...logReturned(genFuncWithReturn())]
// The result
// 值为 ['a', 'b']
上面的代码中,存在两次遍历。第一次是扩展运算符遍历函数logReturned
返回的遍历器对象,第二次是yield*语句
遍历函数genFuncWithReturn
返回的遍历器对象。这两次遍历的效果是叠加的,最终表现为扩展运算符遍历函数genFuncWithReturn
返回的遍历器对象。所以最后的数据表达式得到的值是['a', 'b']
。但是,函数genFuncWithReturn
的return语句
的返回值The result
会返回给函数logReturned
内部的result变量
,因此会有终端输出。
yield*命令
可以很方便地取出嵌套数组的所有成员。
js
function* iterTree (tree) {
if (Array.isArray(tree)) {
for (let i = 0; i < tree.length; i++) {
yield* iterTree(tree[i]);
}
} else {
yield tree;
}
}
const tree = ['a', ['b', 'c'], ['d', 'e']];
for (let x of iterTree(tree)) {
console.log(x)
}
// a
// b
// c
// d
// e
下面是一个稍微复杂的例子,使用yield*语句
遍历完全二叉数
。
js
// 二叉树的构造函数
// 3个参数分别是左子数、当前节点和右子树。
function Tree (left, label, right) {
this.left = left;
this.label = label;
this.right = right;
}
// 中序(inorder)遍历函数
// 由于返回的是一个遍历器,所以要用Generator函数
// 函数体内采用递归算法,所以左子树和右子树要用yield*遍历。
function* inorder (t) {
if (t) {
yield* inorder(t.left);
yield t.label;
yield* inorder(t.right);
}
}
// 生成二叉树
function make (array) {
// 判断是否为叶节点
if (array.length === 1) return new Tree(null, array[0], null);
return new Tree(make(array[0]), array[1], make(array[2]));
}
let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['d']]]);
// 遍历二叉树
var result = [];
for (let node of inorder(tree)) {
result.push(node);
}
console.log(result);
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']
作为对象属性的Generator函数
如果一个对象的属性是Generator函数
,那么可以简写成下面的形式:
js
let obj = {
* myGeneratorMethod () {
...
}
};
上面的代码中,myGeneratorMethod属性
前面有一个星号
,表示这个属性是Generator函数
。
其完整形式如下,与上面的写法是等价的。
js
let obj = {
myGeneratorMethod: function* () {
...
}
}
Generator函数的this
Generator函数
总是返回一个遍历器,ES6规定这个遍历器是Generator函数的实例
,它也继承了Generator函数
的prototype对象
上的写法。
js
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对象
。
js
function* g () {
this.a = 11;
}
let obj = g();
obj.a // undefined
上面的代码中,Generator函数g
在this对象
上添加了一个属性a
,但是obj对象
拿不到这个属性。
js
function* F () {
yield this.x = 2;
yield this.y = 3;
}
上面的代码中,函数F
是一个构造函数,以是一个Generator函数
。这时,使用new命令
就无法生成F的实例
了,因为F
返回的是一个内部指针。
js
'next' in (new F()) // true
由于new F()
返回的是一个Iterator对象
,具有next方法
,所以上面的表达式值为true
。
如果要把Generator函数
当作正常的构造函数使用,可以采用下面的变通方法。首先生成一个空对象,使用bind方法
绑定Generator函数
内部的this
。这样,构造函数调用以后,这个空对象就是Generator函数的实例对象
了。
js
function* F () {
yield this.x = 2;
yield this.y = 3;
}
var obj = {};
var f = F.bind(obj)();
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
obj // {x: 2, y: 3}
上面的代码中,首先F
内部的this对象
绑定obj对象
,然后调用它,返回一个Iterator对象
。这个对象执行了3次next方法
(因为F
内部有2条yield语句),完成F
内部所有代码的运行。这时,所有内部属性都绑定在obj对象
上,因而obj对象
也就成了F的实例
。
Generator函数推导
ES7在数组推导的基础上提出了Generator函数推导
(Generator comprehension)。
js
let generator = function* () {
for (let i = 0; i < 6; i++) {
yield i;
}
}
let squared = ( for (n of generator()) n * n);
// 等同于
let squared = Array.from(generator()).map(n => n * n);
console.log(...squared);
// 0 1 4 9 16 25
“推导”这种语法结构不仅可用于数组,ES7还将其推广到了Generator函数
。for...of循环
会自动调用遍历器的next方法
,将返回值的value属性
作为数组的一个成员。
Generator函数推导
是对数组结构的一种模拟,其最大优点是惰性求值,这样可以保证效率。请看下面的例子:
js
let bigArray = new Array(100000);
for (let i = 0; i < 100000; i++) {
bigArray[i] = i;
}
let first = bigArray.map(n => n * n)[0];
console.log(first);
上面的例子遍历了一个大数组,但是在真正遍历之前,这个数组就已经生成,占用了系统资源。如果改用Generator函数推导
,就能避免这一点。下面的代码只有在用到的时候才会生成一个大数组。
js
let bigGenerator = function* () {
for (let i = 0; i < 100000; i++) {
yield i;
}
}
let squared = (for (n of bigGenerator()) n * n);
console.log(squared.next());
含义
Generator与状态机
Generator
是实现状态机的最佳结构。比如,下面的clock函数
就是一个状态机。
js
var ticking = true;
var clock = function () {
if (ticking) {
console.log('Tick!');
} else {
console.log('Tock!');
}
ticking = !ticking;
}
上面的clock函数
一共有两种状态(Tick
和Tock
),每运行一次,就改变一次状态。这个函数如果用Generator实现
,则如下:
js
var clock = function* (_) {
while (true) {
yield _;
console.log('Tick!');
yield _;
console.log('Tock!');
}
}
对比上面的Generator
实现与ES5
实现,可以看到少了用来保存状态的外部变量ticking
,这样就更简洁,更完全(状态不会被非法篡改),更符合函数式编程的思想,在写法上也更优雅。Generator
之所以不用外部变量保存状态,是因为它本身就包含了一个状态信息,即目前是否处于暂停态。
Generator与协程
协程(coroutine)
是一种程序运行的方式,可以理解成“协作的线程”或“协作的函数”。协程既可以用单线程实现,也可以用多线程实现;前者是一种特殊的子例程,后者是一种特殊的线程。
协程与子例程的差异
传统的“子例程”(subroutine)采用堆栈式“后进先出”的执行方式,只有当调用的子函数完全执行完毕,才会结束执行父函数。协程与其不同,多个线程(单线程情况下即多个函数)可以并行执行,但只有一个线程(或函数)处于正在运行状态,其他线程(或函数)都处于暂停态(suspended),线程(或函数)之间可以交换执行权。也就是说,一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收加执行权时再恢复执行。这种可以并行执行、交换执行权的线程(或函数),就称为协程。
从实现上看,在内存中子例程只使用一个栈(stack),而协程是同时存在多个栈,但是只有一个栈是在运行态。也就是说,协程是以多占用内存为代价实现多任务并行运行。
协程与普通线程的差异
不难看出,协程适用于多任务运行的环境。在这个意义上,它与普通的线程很相似,都有自己的执行上下文,可以分享全局变量。它们的不同之处在于,同一时间可以有多个线程处于运行状态,但是运行的协程只能有一个,其他协程都处于暂停态。此处,普通的线程是抢占式的,到底哪个线程优先得到资源,必须由运行环境决定,但是协程是合作式的,执行权由协程自己分配。
ECMAScript是单线程语言,只能保持一个调用栈。引入协程以后,每个任务可以保持自己的调用栈。这样做的最大好处,就是抛出错误时可以找到原始的调用栈,不至于像异步操作的回调函数那样,一旦出错原始的调用栈早已结束。
Generator函数
是ES6对协程
的实现,但是属于不完全实现。Generator函数
被称为“半协程”(semi-coroutine),意思是只有Generator函数
的调用者才能将程序的执行权还给Generator函数
。如果是完全实现的协程,任何函数都可以让暂停的协程继续执行。
如果将Generator函数
当作协程,完全可以将多个需要互相协作的任务写成Generator函数
,它们之间使用yield语句
交换控制权。
应用
Generator
可以暂停函数执行,返回任意表达式的值。这种特点使得Genertor
有多种应用场景。
异步操作的同步化表达
Generator函数
的暂停执行效果,意味着可以把异步操作写在yield语句
里面,等到调用next方法
时再往后执行。这实际上就等同于不需要写回调函数了,因为异步操作的后续操作可以放在yield语句
下面,反正要等到调用next方法
时再执行。所以,Generator函数
的一个重要实际意义就是用于处理异步操作,改写回调函数。
js
function* loadUI () {
showLoadingScreen();
yield loadUIDataAsynchronously();
hideLoadingScreen();
}
var loader = loadUI();
// 加载UI
loader.next();
// 卸载UI
loader.next();
上面的代码表示,第一次调用loadUI函数
时,该函数不会执行,仅返回一个遍历器。下一次对该遍历器使用next方法
,则会显示加载界面,并且异步加载数据。等到数据加载完成,再一次使用next方法
,则会隐藏加载界面。可以看到,这种写法的好处是所有加载界面的逻辑都被封装在一个函数中,按部就班非常清晰。
AJAX
是典型的异步操作,通过Generator函数
部署AJAX操作
,可以用同步的方式表达。
js
function* main () {
var result = yield request('http://xxx.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
。
下面是另一个例子,通过Generator函数
逐行读取文本文件。
js
function* numbers() {
let file = new FileReader('numbers.txt');
try {
while (!file.eof) {
yield parseInt(file.readLine(), 10);
}
} finally {
file.close();
}
}
上面的代码打开文本文件,使用yield语句
可以手动逐行读取文件。
控制流管理
如果有一个多步操作非常耗时,采用回调函数可能会写成下面这样:
js
step1(function (value) {
step2(function (value2) {
step3(function (value3) {
step4(function (value4) {
// Do something with value4
})
})
})
})
采用Promise
改写上面的代码如下:
js
Q.fcall(step1)
.then(stpe2)
.then(stpe3)
.then(stpe4)
.then(function (value4) {
// Do something with value4
}, function (error) {
// Handle any error from stpe1 through step4
})
.done();
上面的代码已经把回调函数改成了直线执行的形式,但是加入大量的Promise
语法。
Generator函数
可以进一步改善代码流程:
js
function* longRunningTask () {
try {
var value1 = yield step1();
var value2 = yield step2();
var value3 = yield step3();
var value4 = yield step4();
// Do something with value4
} catch (e) {
// Handle any error from stpe1 through step4
}
}
然后,使用一个函数按次序自动执行所有步骤:
js
scheduler(longRunningTask());
function scheduler (task) {
setTimeout(function () {
var taskObj = task.next(tast.value);
// 如果 Generator函数 未结束,就继续调用
if (!taskObj.done) {
task.value = taskObj.value
scheduler(task);
}
}, 0);
}
注意,yield语句
是同步运行,不是异步运行。实际操作中,一般让yield语句
返回Promise对象
。
js
var Q = require('q');
function delay(milliseconds) {
var deferred = Q.defer();
setTimeout(deferred.resolve, milliseconds);
return deferred.promise;
}
function* f () {
yield delay(100);
}
上面的代码使用了Promise
的函数库Q
,yield语句
返回的就是一个Promise对象
。
多个任务按顺序一个接一个执行时,yield语句
可以按顺序排列。多个任何需要并列执行时(比如只有任务A和任务B都执行完,才能执行任务C),可以采用数组的写法。
js
function* parallelDownloads () {
let [text1, text2] = yield [
taskA(),
taskB()
];
console.log(text1, text2);
}
上面的代码中,yield语句
的参数是一个数组,成员就是两个任务——taskA
和taskB
,只有等这两个任务都完成,才会接着执行下面的语句。
部署Iterator接口
利用Generator函数
可以在任意对象上部署Iterator接口
。
js
function* iterEntries (obj) {
let keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
ley key = keys[i];
yield [key, obj[key]];
}
}
let myObj = {foo: 3, bar: 7};
for (let [key, value] of myObj) {
console.log(key, value);
}
// foo 3
// bar 7
下面是一个对数组部署Iterator接口
的例子,尽管数组原生具有这个接口。
js
function* makeSimpleGenerator (array) {
var nextIndex = 0;
while (nextIndex < array.length) {
yield array[nextIndex++];
}
}
var gen = makeSimpleGenerator(['yo', 'ya']);
gen.next().value // 'yo'
gen.next().value // 'ya'
gen.next().done // true
作为数据结构
Generator
可以看作数据结构,更确切地说,可以看作一个数组结构,因为Generator函数
可以返回一系列的值,这意味着它可以对任意表达式提供类似数组的接口。
js
function* doStuff () {
yield fs.readFile.bind(null, 'hello.txt')
yield fs.readFile.bind(null, 'world.txt')
yield fs.readFile.bind(null, 'and-such.txt')
}
上面的代码依次返回3个函数,但是由于使用了Generator函数
,导致可以像处理数组那样处理3个返回的函数。
js
for (task of doStuff()) {
// task 是一个函数,可以像回调函数那样使用它
}
实际上,如果用ES5表达,完成可以用数组模拟Generator
的这种用法。
js
function doStuff () {
return [
fs.readFile.bind(null, 'hello.txt'),
fs.readFile.bind(null, 'world.txt'),
fs.readFile.bind(null, 'and-such.txt')
];
}
上面的函数可以用一模一样的for...of循环
处理。两相比较不难看出,Generator
使得数据或操作具备了类似数组的接口。
以上,摘抄自阮一峰老师的《ES6标准入门》