天天看點

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 使得資料或者操作, 具備了類似數組的接口。

繼續閱讀