es6 javascript 的Generator 函数 (下)

5 Generator.prototype.return()

Generator 函数返回的遍历器对象, 还有一个return方法, 可以返回给定的值, 并且终结遍历 Generator 函数。

function* gen() {
	yield 1;
	yield 2;
	yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }
上面代码中, 遍历器对象g调用return方法后, 返回值的value属性就是return方法的参数foo。 并且, Generator 函数的遍历就终止了, 返回值的done属性为true, 以后再调用next方法, done属性总是返回true。
如果return方法调用时, 不提供参数, 则返回值的value属性为undefined。

function* gen() {
	yield 1;
	yield 2;
	yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return() // { value: undefined, done: true }
如果 Generator 函数内部有try...finally代码块, 那么return方法会推迟到finally代码块执行完再执行。

function* numbers() {
	yield 1;
	try {
		yield 2;
		yield 3;
	} finally {
		yield 4;
		yield 5;
	}
	yield 6;
}
var g = numbers()
g.next() // { done: false, value: 1 }
g.next() // { done: false, value: 2 }
g.return(7) // { done: false, value: 4 }
g.next() // { done: false, value: 5 }
g.next() // { done: true, value: 7 }
上面代码中, 调用return方法后, 就开始执行finally代码块, 然后等到finally代码块执行完, 再执行return方法。


6 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"
上面代码中, foo和bar都是 Generator 函数, 在bar里面调用foo, 是不会有效果的。
这个就需要用到yield * 语句, 用来在一个 Generator 函数里面执行另一个 Generator 函数。

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"
再来看一个对比的例子。

function* inner() {
	yield 'hello!';
}

function* outer1() {
	yield 'open';
	yield inner();
	yield 'close';
}
var gen = outer1()
gen.next().value // "open"
gen.next().value //  返回一个遍历器对象
gen.next().value // "close"
function* outer2() {
	yield 'open'
	yield * inner()
	yield 'close'
}
var gen = outer2()
gen.next().value // "open"
gen.next().value // "hello!"
gen.next().value // "close"
上面例子中, outer2使用了yield * ,outer1没使用。 结果就是, outer1返回一个遍历器对象, outer2返回该遍历器对象的内部值。
从语法角度看, 如果yield命令后面跟的是一个遍历器对象, 需要在yield命令后面加上星号, 表明它返回的是一个遍历器对象。 这被称为yield * 语句。

let delegatedIterator = (function*() {
	yield 'Hello!';
	yield 'Bye!';
}());
let delegatingIterator = (function*() {
	yield 'Greetings!';
	yield * delegatedIterator;
	yield 'Ok, bye.';
}());
for(let value of delegatingIterator) {
	console.log(value);
}
// "Greetings!
// "Hello!"
// "Bye!"
// "Ok, bye."
上面代码中, delegatingIterator是代理者, delegatedIterator是被代理者。 由于yield * delegatedIterator语句得到的值, 是一个遍历器, 所以要用星号表示。 运行结果就是使用一个遍历器, 遍历了多个 Generator 函数, 有递归的效果。
yield * 后面的 Generator 函数( 没有return语句时), 等同于在 Generator 函数内部, 部署一个for...of循环。

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 * 后面的 Generator 函数( 没有return语句时), 不过是for...of的一种简写形式, 完全可以用后者替代前者。 反之, 则需要用var value = yield * iterator的形式获取return语句的值。
如果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 * 遍历。
如果被代理的 Generator 函数有return语句, 那么就可以向代理它的 Generator 函数返回数据。

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提供了返回值。
再看一个例子。

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 * 命令可以很方便地取出嵌套数组的所有成员。

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 * 语句遍历完全二叉树。

	//  下面是二叉树的构造函数,
	//  三个参数分别是左树、当前节点和右树
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', ['g']
	]
]);
//  遍历二叉树
var result = [];
for(let node of inorder(tree)) {
	result.push(node);
}
result
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']


7 作为对象属性的 Generator 函数

如果一个对象的属性是 Generator 函数, 可以简写成下面的形式。

let obj = { *
	myGeneratorMethod() {···}
};
上面代码中, myGeneratorMethod属性前面有一个星号, 表示这个属性是一个 Generator 函数。
它的完整形式如下, 与上面的写法是等价的。

let obj = {
	myGeneratorMethod: function*() {
		// ···
	}
};


8 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.a // undefined
上面代码中, Generator 函数g在this对象上面添加了一个属性a, 但是obj对象拿不到这个属性。
Generator 函数也不能跟new命令一起用, 会报错。

function* F() {
	yield this.x = 2;
	yield this.y = 3;
}
new F()
	// TypeError: F is not a constructor
上面代码中, new命令跟构造函数F一起使用, 结果报错, 因为F不是构造函数。
那么, 有没有办法让 Generator 函数返回一个正常的对象实例, 既可以用next方法, 又可以获得正常的this?
下面是一个变通方法。 首先, 生成一个空对象, 使用bind方法绑定 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


9 含义


9.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 之所以可以不用外部变量保存状态, 是因为它本身就包含了一个状态信息, 即目前是否处于暂停态。


9.2 Generator 与协程

协程( coroutine) 是一种程序运行的方式, 可以理解成“ 协作的线程” 或“ 协作的函数”。 协程既可以用单线程实现, 也可以用多线程实现。 前者是一种特殊的子例程, 后者是一种特殊的线程。
( 1) 协程与子例程的差异
传统的“ 子例程”( subroutine) 采用堆栈式“ 后进先出” 的执行方式, 只有当调用的子函数完全执行完毕, 才会结束执行父函数。 协程与其不同, 多个线程( 单线程情况下, 即多个函数) 可以并行执行, 但是只有一个线程( 或函数) 处于正在运行的状态, 其他线程( 或函数) 都处于暂停态( suspended), 线程( 或函数) 之间可以交换执行权。 也就是说, 一个线程( 或函数) 执行到一半, 可以暂停执行, 将执行权交给另一个线程( 或函数), 等到稍后收回执行权的时候, 再恢复执行。 这种可以并行执行、 交换执行权的线程( 或函数), 就称为协程。
从实现上看, 在内存中, 子例程只使用一个栈( stack), 而协程是同时存在多个栈, 但只有一个栈是在运行状态, 也就是说, 协程是以多占用内存为代价, 实现多任务的并行。
( 2) 协程与普通线程的差异
不难看出, 协程适合用于多任务运行的环境。 在这个意义上, 它与普通的线程很相似, 都有自己的执行上下文、 可以分享全局变量。 它们的不同之处在于, 同一时间可以有多个线程处于运行状态, 但是运行的协程只能有一个, 其他协程都处于暂停状态。 此外, 普通的线程是抢先式的, 到底哪个线程优先得到资源, 必须由运行环境决定, 但是协程是合作式的, 执行权由协程自己分配。
由于 ECMAScript 是单线程语言, 只能保持一个调用栈。 引入协程以后, 每个任务可以保持自己的调用栈。 这样做的最大好处, 就是抛出错误的时候,可以找到原始的调用栈。 不至于像异步操作的回调函数那样, 一旦出错, 原始的调用栈早就结束。
Generator 函数是 ECMAScript 6 对协程的实现, 但属于不完全实现。 Generator 函数被称为“ 半协程”( semi - coroutine), 意思是只有 Generator 函数的调用者, 才能将程序的执行权还给 Generator 函数。 如果是完全执行的协程, 任何函数都可以让暂停的协程继续执行。
如果将 Generator 函数当作协程, 完全可以将多个需要互相协作的任务写成 Generator 函数, 它们之间使用 yield 语句交换控制权。


10 应用

Generator 可以暂停函数执行, 返回任意表达式的值。 这种特点使得 Generator 有多种应用场景。
( 1) 异步操作的同步化表达
Generator 函数的暂停执行的效果, 意味着可以把异步操作写在 yield 语句里面, 等到调用 next 方法时再往后执行。 这实际上等同于不需要写回调函数了, 因为异步操作的后续操作可以放在 yield 语句下面, 反正要等到调用 next 方法时再执行。 所以, Generator 函数的一个重要实际意义就是用来处理异步操作, 改写回调函数。

function* loadUI() {
	showLoadingScreen();
	yield loadUIDataAsynchronously();
	hideLoadingScreen();
}
var loader = loadUI();
//  加载 UI
loader.next()
	//  卸载 UI
loader.next()
上面代码表示, 第一次调用 loadUI 函数时, 该函数不会执行, 仅返回一个遍历器。 下一次对该遍历器调用 next 方法, 则会显示 Loading 界面, 并且异步加载数据。 等到数据加载完成, 再一次使用 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。
下面是另一个例子, 通过 Generator 函数逐行读取文本文件。

function* numbers() {
	let file = new FileReader("numbers.txt");
	try {
		while(!file.eof) {
			yield parseInt(file.readLine(), 10);
		}
	} finally {
		file.close();
	}
}
上面代码打开文本文件, 使用 yield 语句可以手动逐行读取文件。

( 2) 控制流管理
如果有一个多步操作非常耗时, 采用回调函数, 可能会写成下面这样。

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)
	.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
	}
}
然后, 使用一个函数, 按次序自动执行所有步骤。

scheduler(longRunningTask(initialValue));
function scheduler(task) {
	var taskObj = task.next(task.value);
	//  如果 Generator 函数未结束,就继续调用
	if(!taskObj.done) {
		task.value = taskObj.value
		scheduler(task);
	}
}
注意, 上面这种做法, 只适合同步操作, 即所有的task都必须是同步的, 不能有异步操作。 因为这里的代码一得到返回值, 就继续往下执行, 没有判断异步操作何时完成。 如果要控制异步的操作流程, 详见后面的《 异步操作》 一章。
下面, 利用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();
}
(3) 部署 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
上述代码中, myObj是一个普通对象, 通过iterEntries函数, 就有了 Iterator 接口。 也就是说, 可以在任意对象上部署next方法。
下面是一个对数组部署 Iterator 接口的例子, 尽管数组原生具有这个接口。

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
( 4) 作为数据结构

Generator 可以看作是数据结构, 更确切地说, 可以看作是一个数组结构, 因为 Generator 函数可以返回一系列的值, 这意味着它可以对任意表达式,提供类似数组的接口。

function* doStuff() {
	yield fs.readFile.bind(null, 'hello.txt');
	yield fs.readFile.bind(null, 'world.txt');
	yield fs.readFile.bind(null, 'and-such.txt');
}
上面代码就是依次返回三个函数, 但是由于使用了 Generator 函数, 导致可以像处理数组那样, 处理这三个返回的函数。

for(task of doStuff()) {
	// task 是一个函数,可以像回调函数那样使用它
}
实际上, 如果用 ES5 表达, 完全可以用数组模拟 Generator 的这种用法。

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 使得数据或者操作, 具备了类似数组的接口。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值