
Generator
Generator 函數(shù)是 es6 中的新的異步編程解決方案,本節(jié)僅討論 Generator 函數(shù)本身,異步編程放在后面的部分。
Generator 函數(shù)之前也提到過,描述內(nèi)部封裝的多個(gè)狀態(tài),類似一個(gè)狀態(tài)機(jī),當(dāng)然也是很好的 iterator 生成器。Generator 函數(shù)的基本形式如下:
function* gen(){
yield status1;
yield status2;
//...
}
不難看出,Generator 函數(shù)在 function 關(guān)鍵字和函數(shù)名之間加了一個(gè)星號"*", 內(nèi)部用 yield 返回每一個(gè)狀態(tài)。
當(dāng)然還有其他格式的定義:
//函數(shù)表達(dá)式
var gen = function*(){
yield status1;
//...
};
//對象方法
var obj = {
*gen(){
yield status1;
//...
}
};
Generator 函數(shù)調(diào)用時(shí),寫法和普通函數(shù)一樣。但函數(shù)并不執(zhí)行執(zhí)行時(shí),返回內(nèi)部自帶 iterator,之后調(diào)用該 iterator 的 next() 方法, 函數(shù)會開始執(zhí)行,函數(shù)每次執(zhí)行遇到 yield 關(guān)鍵字返回對應(yīng)狀態(tài),并跳出函數(shù),當(dāng)下一次再次調(diào)用 next() 的時(shí)候,函數(shù)會繼續(xù)從上一次 yield 跳出的下一跳語句繼續(xù)執(zhí)行。當(dāng)然 Generator 函數(shù)也可以用 return 返回狀態(tài),不過此時(shí),函數(shù)就真的運(yùn)行結(jié)束了,該遍歷器就不再工作了;如果函數(shù)內(nèi)部所以的 yield 都執(zhí)行完了,該遍歷器一樣不再工作了:
function* gen(){
yield "hello";
yield "world";
return "ending";
}
var it = gen();
console.log(it.next()); //{value: "hello", done: false}
console.log(it.next()); //{value: "world", done: false}
console.log(it.next()); //{value: "ending", done: true}
console.log(it.next()); //{value: undefined, done: true}
注意:
- return 返回的值,對應(yīng)的 done 屬性是 true。說明 return語句結(jié)束了遍歷,iterator 不再繼續(xù)遍歷,即便后面還有代碼和 yield。
- Generator 函數(shù)可以沒有 yield 返回值,此時(shí)它依然返回一個(gè) iterator, 并且在 iterator 調(diào)用 next 方法時(shí)一次行執(zhí)行完函數(shù)內(nèi)全部代碼,返回
{value: undefined, done: true}。 如果有 return 語句,該返回值對應(yīng)的 value 屬性值為 return 表達(dá)式的值。 - 普通函數(shù)使用 yield 語句會報(bào)錯(cuò)
- yield 可以用作函數(shù)參數(shù),表達(dá)式參數(shù):
function* gen(){
console.log("hello" + (yield)); //yield 用作表達(dá)式參數(shù)必須加()
let input = yield;
foo(yield 'a', yield 'b');
}
- Generator 函數(shù)的默認(rèn)遍歷器
[Symbol.iterator]是函數(shù)自己:
function* gen(){}
var g = gen()
g[Symbol.iterator]() === g; //true
next() 參數(shù)
yield 語句本身具有返回值,返回值是下一次調(diào)用 next 方法是傳入的值。next 方法接受一個(gè)參數(shù),默認(rèn) undefined:
function* f(){
for(let i = 0; true; i++){
var reset = yield i;
if(reset) i = -1;
}
}
var g = f();
console.log(g.next().value) //0
console.log(g.next().value) //1
console.log(g.next().value) //2
console.log(g.next(true).value) //0
上面 代碼第3行var reset = yield i等號右側(cè)是利用 yield 返回i, 由于賦值運(yùn)算時(shí)右結(jié)合的,返回 i 以后,函數(shù)暫停執(zhí)行,賦值工作沒有完成。之后再次調(diào)用 next 方法時(shí),將這次傳入?yún)?shù)作為剛才這個(gè) yield 的返回值賦給了 reset, 因此計(jì)數(shù)器被重置。
function* foo(x){
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var g = foo(5);
console.log(g.next()); //{value: 6, done: false}
console.log(g.next(12)); //{value: 8, done: false}
console.log(g.next(13)); //{value: 42, done: true}
第一次調(diào)用 next 函數(shù)不需要參數(shù),作為 Generator 啟動,如果帶了參數(shù)也會被忽略。當(dāng)然,如果一定想在第一次調(diào)用 next 時(shí)候就賦值,可以將 Generator 函數(shù)封裝一下:
//一種不完善的思路,通常不強(qiáng)求這樣做
function wrapper(gen){
return function(){
let genObj = gen(...arguments);
genObj.next(); //提前先啟動一次,但如果此時(shí)帶有返回值,該值就丟了!
return genObj;
}
}
var gen = wrapper(function*(){
console.log(`first input: "${yield}"`);
});
var it = gen();
it.next("Bye-Bye"); //first input: "Bye-Bye"
for...of
我們注意到,之前在 iterator 中,迭代器最后返回{value: undefined, done: true},其中值為 undefined 和 done 為 true 是同時(shí)出現(xiàn)的,而遍歷結(jié)果不包含 done 為 true 時(shí)對應(yīng)的 value 值,所以 Generator 的 for...of 循環(huán)最好不要用 return 返回值,因?yàn)樵撝祵⒉粫槐闅v:
function* gen(){
for(var i = 0; i < 5; i++){
yield i;
}
return 5;
}
for(let v of gen()){
console.log(v); //依次輸出 0, 1, 2, 3, 4, 沒有 5
}
除了 for...of, Generator 還有很多簡單用法。下面利用 fibonacci 數(shù)列,演示幾種不同的 Generator 用法:
- 展開運(yùn)算符
function* fib(n = Infinity){
var a = 1, b = 1;
while(n){
yield a;
[a, b] = [b, a + b];
n--;
}
}
console.log([...fib(10)]); //1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
- 解構(gòu)賦值
function* fib(n = Infinity){
var a = 1, b = 1;
while(n){
yield a;
[a, b] = [b, a + b];
n--;
}
}
var [a, b, c, d, e, f] = fib(); //a=1, b=1, c=2, d=3, e=5, f=8
- 構(gòu)造函數(shù)參數(shù)
function* fib(n = Infinity){
var a = 1, b = 1;
while(n){
yield a;
[a, b] = [b, a + b];
n--;
}
}
var set = new Set(fib(n));
console.log(set); //Set(9) [1, 2, 3, 5, 8, 13, 21, 34, 55]
- Array.from方法
function* fib(n = Infinity){
var a = 1, b = 1;
var n = 10;
while(n){
yield a;
[a, b] = [b, a + b];
n--;
}
}
var arr = Array.from(fib(10));
console.log(arr); //[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
- 遍歷對象
function* entries(obj){
for(let key of Object.keys(obj)){
yield [key, obj[key]];
}
}
var obj = {
red: "#ff0000",
green: "#00ff00",
blue: "#0000ff"
};
for(let [key, value] of entries(obj)){
console.log(`${key}: ${value}`); //依次輸出 "red: #ff0000", "green: #00ff00", "blue: #0000ff"
}
throw() 方法和 return() 方法
Generator 返回的遍歷器對象具throw() 方法, 一般的遍歷器用不到這個(gè)方法。該方法接受一個(gè)參數(shù)作為拋出的錯(cuò)誤,該錯(cuò)誤可以在 Generator 內(nèi)部捕獲:
function* gen(){
while(1){
try{
yield "OK";
} catch(e) {
if(e === 'a') console.log(`內(nèi)部捕獲: ${e}`); //內(nèi)部捕獲: a
else throw e;
}
}
}
var it = gen();
it.next(); //如果沒有這一行啟動生成器,結(jié)果僅輸出:外部捕獲: a
try{
it.throw('a');
it.throw('b');
it.next(); //上一行錯(cuò)誤為外部捕獲,try 中的代碼不在繼續(xù)執(zhí)行,故這一行不執(zhí)行
} catch(e) {
console.log(`外部捕獲: ${e}`) //外部捕獲: b
}
throw參數(shù)在傳遞過程中和 next 參數(shù)類似,需要先調(diào)用一次 next 方法啟動生成器,之后拋出的錯(cuò)誤會在前一個(gè) yield 的位置被捕獲:
function* gen(){
yield "OK"; //錯(cuò)誤被拋到這里,不在內(nèi)部 try 語句內(nèi)無法捕獲
while(1){
try{
yield "OK";
} catch(e) {
console.log(`內(nèi)部捕獲: ${e}`);
}
}
}
var it = gen();
it.next();
try{
it.throw('a');
} catch(e) {
console.log(`外部捕獲: ${e}`) //外部捕獲: a
}
注意: 不要混用 throw() 方法和 throw 語句,后者無法將錯(cuò)誤拋到生成器內(nèi)部。其次,throw 會終止遍歷器,不能繼續(xù)工作,而 throw 不會終止遍歷器:
function* gen(){
yield console.log("hello");
yield console.log("world");
}
//throw 語句
var it1 = gen();
it1.next(); //hello
try{
throw new Error();
} catch(e) {
it1.next() //world
}
//throw() 方法
var it2 = gen();
it2.next(); //hello
try{
it2.throw();
} catch(e) {
it2.next() //遍歷器被關(guān)閉無法執(zhí)行, 靜默失敗
}
如果在遍歷器內(nèi)部拋出錯(cuò)誤,遍歷器中止,繼續(xù)調(diào)用 next() 方法將得到{value: undefined, done: true}:
function* gen(){
var x = yield "ok";
var y = yield x.toUpperCase();
var z = yield (x + y + z);
}
//throw 語句
var it = gen();
it.next(); //"ok"
try{
it.next();
} catch(e) {
console.log("Error Caught"); //Error Caught
} finally {
it.next(); //{value: undefined, done: true}
}
return() 方法返回指定的值,并終止迭代器:
var it = (function* gen(){
yield 1;
yield 2;
yield 3;
}());
console.log(it.next()); //{value: 1, done: false}
console.log(it.next()); //{value: 2, done: false}
console.log(it.return("end")); //{value: "end", done: true}
console.log(it.next()); //{value: undefined, done: true}
如果不給 return() 方法提供參數(shù),默認(rèn)是 undefined
如果 Generator 中有 try...finally 語句,return 會在 finally 執(zhí)行完再執(zhí)行:
function* numbers(){
yield 1;
try{
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
var g = numbers();
console.log(g.next().value); //1
console.log(g.next().value); //2
console.log(g.return("end").value); //延遲到 finally 之后輸出 -----
console.log(g.next().value); //4 |
console.log(g.next().value); //5 |
//"end" <-------------------
console.log(g.next().value); //undefined
yield* 語句
在一個(gè) Generator 中調(diào)用另一個(gè) Generator 函數(shù)默認(rèn)是沒有效果的:
function* gen(){
yield 3;
yield 2;
}
function* fun(){
yield gen();
yield 1;
}
var it = fun();
console.log(it.next().value); //gen 函數(shù)返回的遍歷器
console.log(it.next().value); //1
console.log(it.next().value); //undefined
顯然第一次返回的結(jié)果不是我們想要的。需要使用 yield* 解決這個(gè)問題。yield* 將一個(gè)可遍歷結(jié)構(gòu)解構(gòu),并逐一返回其中的數(shù)據(jù)。
function* gen(){
yield 3;
yield 2;
}
function* fun(){
yield* gen();
yield 1;
}
var it = fun();
console.log(it.next().value); //3
console.log(it.next().value); //2
console.log(it.next().value); //1
function* fun(){
yield* [4,3,2];
yield 1;
}
var it = fun();
console.log(it.next().value); //4
console.log(it.next().value); //3
console.log(it.next().value); //2
console.log(it.next().value); //1
被代理的 Generator 可以用return向代理它的 Generator 返回值:
function* gen(){
yield "Bye";
yield* "Hi"
return 2;
}
function* fun(){
if((yield* gen()) === 2) yield* "ok";
else yield "ok";
}
var it = fun();
console.log(it.next().value); //Bye
console.log(it.next().value); //H
console.log(it.next().value); //i
console.log(it.next().value); //o
console.log(it.next().value); //k
console.log(it.next().value); //undefined
舉例:
- 數(shù)組扁平化
//方法1:
var arr = [1,2,[2,[3,4],2],[3,4,[3,[6]]]];
function plat(arr){
var temp = [];
for(let v of arr){
if(Array.isArray(v)){
plat(v);
} else {
temp.push(v);
}
}
return temp;
}
console.log(plat(arr)); //[1, 2, 2, 3, 4, 2, 3, 4, 3, 6]
//方法2:
function* plat2(arr){
for(let v of arr){
if(Array.isArray(v)){
yield* plat2(v);
} else {
yield v;
}
}
}
var temp = [];
for(let x of plat2(arr)){
temp.push(x);
}
console.log(temp); //[1, 2, 2, 3, 4, 2, 3, 4, 3, 6]
- 遍歷二叉樹
//節(jié)點(diǎn)
function Node(value, left, right){
this.value = value;
this.left = left;
this.right = right;
}
//二叉樹
function Tree(arr){
if(arr.length === 1){
return new Node(arr[0], null, null);
} else {
return new Node(arr[1], Tree(arr[0]), Tree(arr[2]));
}
}
var tree = Tree([[[1], 4, [5]], 2, [[[0], 6, [9]], 8, [7]]]);
//前序遍歷
function* preorder(tree){
if(tree){
yield tree.value;
yield* preorder(tree.left);
yield* preorder(tree.right);
}
}
//中序遍歷
function* inorder(tree){
if(tree){
yield* inorder(tree.left);
yield tree.value;
yield* inorder(tree.right);
}
}
//后序遍歷
function* postorder(tree){
if(tree){
yield* postorder(tree.left);
yield* postorder(tree.right);
yield tree.value;
}
}
var _pre = [], _in = [], _post = [];
for(let v of preorder(tree)){
_pre.push(v);
}
for(let v of inorder(tree)){
_in.push(v);
}
for(let v of postorder(tree)){
_post.push(v);
}
console.log(_pre); //[2, 4, 1, 5, 8, 6, 0, 9, 7]
console.log(_in); //[1, 4, 5, 2, 0, 6, 9, 8, 7]
console.log(_post); //[1, 5, 4, 0, 9, 6, 7, 8, 2]
- Generator 實(shí)現(xiàn)狀態(tài)機(jī):
//傳統(tǒng)實(shí)現(xiàn)方法
var clock1 = function(){
var ticking = false;
return {
next: function(){
ticking = !ticking;
if(ticking){
return "Tick";
}else{
return "Tock";
}
}
}
};
var ck1 = clock1();
console.log(ck1.next()); //Tick
console.log(ck1.next()); //Tock
console.log(ck1.next()); //Tick
//Generator 方法
var clock2 = function*(){
while(1){
yield "Tick";
yield "Tock";
}
};
var ck2 = clock2();
console.log(ck2.next().value); //Tick
console.log(ck2.next().value); //Tock
console.log(ck2.next().value); //Tick
Generator 函數(shù)中的 this
在ES6中, 規(guī)定了所有 iterator 是 Generator 函數(shù)的實(shí)例:
function* gen(){}
var it = gen();
it instanceof gen; //true
console.log(gen.__proto__); //GeneratorFunction
console.log(gen.__proto__.__proto__); //Function
console.log(gen.constructor); //GeneratorFunction
console.log(gen.__proto__.constructor); //GeneratorFunction
gen.prototype.sayHello = function(){
console.log("hello");
}
it.sayHello(); //"hello"
但是 Generator 函數(shù)中的 this 并不指向生成的 iterator:
function* gen(){
this.num = 11;
console.log(this);
}
var it = gen();
console.log(it.num); //undefined
it.next(); //Window
var obj = {
* fun(){
console.log(this);
}
}
var o_it = obj.fun();
o_it.next(); //obj
由上面這個(gè)例子不難看出,Generator 函數(shù)中的 this 和普通函數(shù)是一樣的。不過,可不可以把 Generator 函數(shù)作為構(gòu)造函數(shù)呢?顯然是不行的:
function* gen(){
this.num = 11;
}
gen.prototype.say = function(){console.log("hello")}
var a = new gen(); //TypeError: gen is not a constructor
Generator 函數(shù)推導(dǎo)
ES7 在數(shù)組推導(dǎo)的基礎(chǔ)上提出了 Generator 函數(shù)推導(dǎo),可惜這個(gè)功能目前還不能使用:
let gen = function*(){
for(let i = 0; i < 6; i++){
yield i;
}
};
let arr = [for(let n of gen()) n * n];
//相當(dāng)于:
let arr = Array.from(gen()).map(n => n * n);
console.log(arr); [0,1,4,9,16,25]
Generator 數(shù)組推導(dǎo),利用惰性求值優(yōu)化系統(tǒng)資源利用:
var bigArr = new Array(10000);
for(let i = 0; i < 10000; i++){
bigArr.push(i);
}
//....其他代碼
//使用 bigArr 之前很久就分配了內(nèi)存
console.log(bigArr[100]);
var gen = function*(){
for(let i = 0; i < 10000; i++){
yield i;
}
};
//....其他代碼
//使用 bigArr 時(shí)才分配內(nèi)存
var bigArr = [for(let n of gen()) n];
console.log(bigArr[100]);
應(yīng)用舉例
優(yōu)化回調(diào)函數(shù)
//偽代碼
function* main(){
var result = yield request("http://url.com");
var res = JSON.parse(result);
console.log(res.value);
}
function request(url){
var xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onreadystatechange = function(){
if(xhr.readyState == 4 && xhr.status == 200){
it.next(xhr.response);
}
}
xhr.send();
}
var it = main();
it.next();
另一個(gè)例子:
//偽代碼
//遇到多重回調(diào)函數(shù),傳統(tǒng)寫法:
step1(function(value1){
step2(value1, function(value2){
step3(value2, function(value3){
step4(value3, function(value4){
//do something
});
});
});
});
//利用 Generator 寫:
function* gen(){
try{
var value1 = yield step1();
var value2 = yield step2(value1);
var value3 = yield step3(value2);
var value4 = yield step4(value3);
} catch(e) {
//Handle the error form step1 to step4
}
}