ECMAScript6(13):Generator 函數

Generator

Generator 函數是 es6 中的新的異步編程解決方案,本節僅討論 Generator 函數自己,異步編程放在後面的部分。
Generator 函數以前也提到過,描述內部封裝的多個狀態,相似一個狀態機,固然也是很好的 iterator 生成器。Generator 函數的基本形式以下:es6

function* gen(){
  yield status1;
  yield status2;
  //...
}

不難看出,Generator 函數在 function 關鍵字和函數名之間加了一個星號"*", 內部用 yield 返回每個狀態。編程

固然還有其餘格式的定義:數組

//函數表達式
var gen = function*(){
  yield status1;
  //...
};

//對象方法
var obj = {
  *gen(){
    yield status1;
    //...
  }
};

Generator 函數調用時,寫法和普通函數同樣。但函數並不執行執行時,返回內部自帶 iterator,以後調用該 iterator 的 next() 方法, 函數會開始執行,函數每次執行遇到 yield 關鍵字返回對應狀態,並跳出函數,當下一次再次調用 next() 的時候,函數會繼續從上一次 yield 跳出的下一跳語句繼續執行。固然 Generator 函數也能夠用 return 返回狀態,不過此時,函數就真的運行結束了,該遍歷器就再也不工做了;若是函數內部因此的 yield 都執行完了,該遍歷器同樣再也不工做了:app

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 返回的值,對應的 done 屬性是 true。說明 return語句結束了遍歷,iterator 再也不繼續遍歷,即使後面還有代碼和 yield。
  • Generator 函數能夠沒有 yield 返回值,此時它依然返回一個 iterator, 而且在 iterator 調用 next 方法時一次行執行完函數內所有代碼,返回{value: undefined, done: true}。 若是有 return 語句,該返回值對應的 value 屬性值爲 return 表達式的值。
  • 普通函數使用 yield 語句會報錯
  • yield 能夠用做函數參數,表達式參數:
function* gen(){
  console.log("hello" + (yield));    //yield 用做表達式參數必須加()
  let input = yield;
  foo(yield 'a', yield 'b');
}
  • Generator 函數的默認遍歷器[Symbol.iterator]是函數本身:
function* gen(){}
var g = gen()
g[Symbol.iterator]() === g;    //true

next() 參數

yield 語句自己具備返回值,返回值是下一次調用 next 方法是傳入的值。next 方法接受一個參數,默認 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等號右側是利用 yield 返回i, 因爲賦值運算時右結合的,返回 i 之後,函數暫停執行,賦值工做沒有完成。以後再次調用 next 方法時,將此次傳入參數做爲剛纔這個 yield 的返回值賦給了 reset, 所以計數器被重置。函數

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}

第一次調用 next 函數不須要參數,做爲 Generator 啓動,若是帶了參數也會被忽略。固然,若是必定想在第一次調用 next 時候就賦值,能夠將 Generator 函數封裝一下:post

//一種不完善的思路,一般不強求這樣作
function wrapper(gen){
  return function(){
    let genObj = gen(...arguments);
    genObj.next();       //提早先啓動一次,但若是此時帶有返回值,該值就丟了!
    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 是同時出現的,而遍歷結果不包含 done 爲 true 時對應的 value 值,因此 Generator 的 for...of 循環最好不要用 return 返回值,由於該值將不會被遍歷:優化

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 數列,演示幾種不一樣的 Generator 用法:this

  • 展開運算符
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]
  • 解構賦值
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
  • 構造函數參數
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() 方法, 通常的遍歷器用不到這個方法。該方法接受一個參數做爲拋出的錯誤,該錯誤能夠在 Generator 內部捕獲:

function* gen(){
  while(1){
    try{
      yield "OK";
    } catch(e) {
      if(e === 'a') console.log(`內部捕獲: ${e}`);    //內部捕獲: a
      else throw e;
    }
  }
}
var it = gen();
it.next();              //若是沒有這一行啓動生成器,結果僅輸出:外部捕獲: a
try{
  it.throw('a');
  it.throw('b');
  it.next();            //上一行錯誤爲外部捕獲,try 中的代碼不在繼續執行,故這一行不執行
} catch(e) {
  console.log(`外部捕獲: ${e}`)    //外部捕獲: b
}

throw參數在傳遞過程當中和 next 參數相似,須要先調用一次 next 方法啓動生成器,以後拋出的錯誤會在前一個 yield 的位置被捕獲:

function* gen(){
  yield "OK";             //錯誤被拋到這裏,不在內部 try 語句內沒法捕獲
  while(1){
    try{
      yield "OK";
    } catch(e) {
      console.log(`內部捕獲: ${e}`);
    }
  }
}
var it = gen();
it.next();
try{
  it.throw('a');
} catch(e) {
  console.log(`外部捕獲: ${e}`)    //外部捕獲: a
}

注意: 不要混用 throw() 方法和 throw 語句,後者沒法將錯誤拋到生成器內部。其次,throw 會終止遍歷器,不能繼續工做,而 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()         //遍歷器被關閉沒法執行, 靜默失敗
}

若是在遍歷器內部拋出錯誤,遍歷器停止,繼續調用 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() 方法提供參數,默認是 undefined
若是 Generator 中有 try...finally 語句,return 會在 finally 執行完再執行:

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* 語句

在一個 Generator 中調用另外一個 Generator 函數默認是沒有效果的:

function* gen(){
  yield 3;
  yield 2;
}
function* fun(){
  yield gen();
  yield 1;
}
var it = fun();
console.log(it.next().value);    //gen 函數返回的遍歷器
console.log(it.next().value);    //1
console.log(it.next().value);    //undefined

顯然第一次返回的結果不是咱們想要的。須要使用 yield 解決這個問題。yield 將一個可遍歷結構解構,並逐一返回其中的數據。

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

舉例:

  1. 數組扁平化
//方法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]
  1. 遍歷二叉樹
//節點
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]
  1. Generator 實現狀態機:
//傳統實現方法
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 函數中的 this

在ES6中, 規定了全部 iterator 是 Generator 函數的實例:

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 函數中的 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

由上面這個例子不難看出,Generator 函數中的 this 和普通函數是同樣的。不過,可不能夠把 Generator 函數做爲構造函數呢?顯然是不行的:

function* gen(){
  this.num = 11;
}
gen.prototype.say = function(){console.log("hello")}
var a = new gen();    //TypeError: gen is not a constructor

Generator 函數推導

ES7 在數組推導的基礎上提出了 Generator 函數推導,惋惜這個功能目前還不能使用:

let gen = function*(){
  for(let i = 0; i < 6; i++){
    yield i;
  }
};
let arr = [for(let n of gen()) n * n];
//至關於:
let arr = Array.from(gen()).map(n => n * n);
console.log(arr); [0,1,4,9,16,25]

Generator 數組推導,利用惰性求值優化系統資源利用:

var bigArr = new Array(10000);
for(let i = 0; i < 10000; i++){
  bigArr.push(i);
}
//....其餘代碼
//使用 bigArr 以前好久就分配了內存
console.log(bigArr[100]);

var gen = function*(){
  for(let i = 0; i < 10000; i++){
    yield i;
  }
};
//....其餘代碼
//使用 bigArr 時才分配內存
var bigArr = [for(let n of gen()) n];
console.log(bigArr[100]);

應用舉例

  • 優化回調函數
//僞代碼
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();

另外一個例子:

//僞代碼
//遇到多重回調函數,傳統寫法:
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
  }
}
相關文章
相關標籤/搜索