1. 程式人生 > >ES6學習筆記14 Generator 函式

ES6學習筆記14 Generator 函式

簡介

Generator 函式有多種理解角度。語法上,首先可以把它理解成,Generator 函式是一個狀態機,封裝了多個內部狀態。

執行 Generator 函式會返回一個遍歷器物件,也就是說,Generator 函式除了狀態機,還是一個遍歷器物件生成函式。返回的遍歷器物件,可以依次遍歷 Generator 函式內部的每一個狀態。

形式上,Generator 函式是一個普通函式,但是有兩個特徵。一是,function關鍵字與函式名之間有一個星號;二是,函式體內部使用yield表示式,定義不同的內部狀態。

function* helloWorldGenerator() {
  yield
'hello'; yield 'world'; return 'ending'; } var hw = helloWorldGenerator();

呼叫 Generator 函式後,該函式並不執行,返回的也不是函式執行結果,而是一個指向內部狀態的指標物件,也就是遍歷器物件

下一步,必須呼叫遍歷器物件的next方法,使得指標移向下一個狀態。每次呼叫next方法,內部指標就從函式頭部或上一次停下來的地方開始執行,直到遇到下一個yield表示式(或return語句)為止,如果沒有再遇到新的yield表示式,就一直執行到函式結束。換言之,Generator 函式是分段執行的,yield

表示式是暫停執行的標記,而next方法可以恢復執行。

hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }

yield表示式

yield表示式後面的表示式,只有當呼叫next方法、內部指標指向該語句時才會執行,因此等於為 JavaScript 提供了手動的“惰性求值”(Lazy Evaluation)的語法功能。

function* gen() {
  yield  123 + 456;  //後面的表示式只有當呼叫next方法時,才會計算求值
}

yield表示式只能用在 Generator 函式裡面,用在其他地方都會報錯。另外,yield表示式如果用在另一個表示式之中,必須放在圓括號裡面

function* demo() {
  console.log('Hello' + yield); // SyntaxError
  console.log('Hello' + yield 123); // SyntaxError

  console.log('Hello' + (yield)); // OK
  console.log('Hello' + (yield 123)); // OK
}

yield表示式用作函式引數或放在賦值表示式的右邊,可以不加括號

function* demo() {
  foo(yield 'a', yield 'b'); // OK
  let input = yield; // OK
}

與Iterator介面的關係

由於 Generator 函式就是遍歷器生成函式,因此可以把 Generator 賦值給物件的Symbol.iterator屬性,從而使得該物件具有 Iterator 介面。

var myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};

[...myIterable] // [1, 2, 3]

Generator 函式執行後,返回一個遍歷器物件。該物件本身也具有Symbol.iterator屬性,執行後返回自身。

function* gen(){
  // some code
}

var g = gen();

g[Symbol.iterator]() === g
// true

next方法的引數

yield表示式本身沒有返回值,或者說總是返回undefined。next方法可以帶一個引數,該引數就會被當作上一個yield表示式的返回值。

function* f() {
  for(var i = 0; true; i++) {
    var reset = yield i;     //reset為yield表示式的返回值
    if(reset) { i = -1; }    //當reset不為undefined時,i賦值為-1
  }
}

var g = f();

g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }

這個功能有很重要的語法意義。Generator 函式從暫停狀態到恢復執行,它的上下文狀態(context)是不變的。通過next方法的引數,就有辦法在 Generator 函式開始執行之後,繼續向函式體內部注入值。

注意,由於next方法的引數表示上一個yield表示式的返回值,所以在第一次使用next方法時,傳遞引數是無效的。要想在第一次呼叫時就能輸入值,可以在 Generator 函式外面再包一層。

function wrapper(generatorFunction) {
  return function (...args) {
    let generatorObject = generatorFunction(...args);  //呼叫原始的Generator函式
    generatorObject.next();   //先執行一次next方法
    return generatorObject;   //返回遍歷器物件
  };
}

const wrapped = wrapper(function* () {
  console.log(`First input: ${yield}`);
  return 'DONE';
});

wrapped().next('hello!')
// First input: hello!

上述程式碼在wrapper函式中,首先呼叫一次next方法,再返回遍歷器物件。當用戶自己呼叫next方法時,看起來就像是第一次呼叫,但實際上,這是第二次呼叫next方法。

for…of 迴圈

for...of迴圈可以自動遍歷 Generator 函式時生成的Iterator物件,且此時不再需要呼叫next方法,但是遍歷不包含return語句的返回值

function* foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5

除了for...of迴圈以外,擴充套件運算子(...)、解構賦值和Array.from方法內部呼叫的,都是遍歷器介面。這意味著,它們都可以將 Generator 函式返回的 Iterator 物件,作為引數。

function* numbers () {
  yield 1
  yield 2
  return 3
  yield 4
}

// 擴充套件運算子
[...numbers()] // [1, 2]

// Array.from 方法
Array.from(numbers()) // [1, 2]

// 解構賦值
let [x, y] = numbers();
x // 1
y // 2

// for...of 迴圈
for (let n of numbers()) {
  console.log(n)
}
// 1
// 2

Generator.prototype.throw() 和 Generator.prototype.return()

Generator 函式返回的遍歷器物件,都有一個throw方法,可以在函式體外丟擲錯誤,然後在 Generator 函式體內捕獲。throw方法可以接受一個引數,建議丟擲Error物件的例項。

var g = function* () {
  try {
    yield;
  } catch (e) {
    console.log('內部捕獲', e);
  }
};

var i = g();
i.next();

try {
  i.throw('a');
  i.throw('b');
} catch (e) {
  console.log('外部捕獲', e);
}
// 內部捕獲 a
// 外部捕獲 b

遍歷器物件的throw方法與全域性的throw命令相比,前者丟擲的錯誤可以被Generator函式內部的catch語句(優先捕獲)和函式外部的catch語句捕獲,後者丟擲的錯誤只能被函式體外的catch語句捕獲

var g = function* () {
  while (true) {
    yield;
    console.log('內部捕獲', e);
  }
};

var i = g();
i.next();

try {
  i.throw('a');
  i.throw('b');
} catch (e) {
  console.log('外部捕獲', e);
}
// 外部捕獲 a

如果 Generator 函式內部和外部,都沒有部署try...catch程式碼塊,那麼throw方法將導致程式報錯,直接中斷執行。

throw方法被捕獲以後,會附帶執行下一條yield表示式。也就是說,會附帶執行一次next方法。

var gen = function* gen(){
  try {
    yield console.log('a');
  } catch (e) {
    // ...
  }
  yield console.log('b');
  yield console.log('c');
}

var g = gen();
g.next() // a
g.throw() // b
g.next() // c

Generator 函式體外丟擲的錯誤,可以在函式體內捕獲;反過來,Generator 函式體內丟擲的錯誤,也可以被函式體外的catch捕獲。一旦 Generator 執行過程中丟擲錯誤,且沒有被內部捕獲,就不會再執行下去了。如果此後還呼叫next方法,將返回一個value屬性等於undefineddone屬性等於true的物件,即 JavaScript 引擎認為這個 Generator 已經執行結束了。

function* foo() {
  var x = yield 3;
  var y = x.toUpperCase();  //報錯
  yield y;
}

var it = foo();

it.next(); // { value:3, done:false }

try {
  it.next(42);
} catch (err) {
  console.log(err);   //捕獲
}

Generator函式返回的遍歷器物件,還有一個return方法,可以返回給定的值,並且終結遍歷 Generator 函式。如果不提供引數,則返回undefined

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 }

如果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() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }

比較遍歷器物件的next()throw()return()方法,它們的作用都是讓Generator函式恢復執行,並且使用不同的語句替換yield表示式。next()是將yield表示式替換成一個值,throw()是將yield表示式替換成一個throw語句,而return()是將yield表示式替換成一個return語句。

yield* 表示式

如果在 Generator 函式內部,呼叫另一個 Generator 函式,預設情況下是沒有效果的。而yield*表示式,用來在一個 Generator 函式裡面執行另一個 Generator 函式。yield*後面跟的是一個遍歷器物件

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"

如果yield*後面跟著資料結構,只要該資料結構具有Iterator介面,就可以被yield*遍歷

function* gen(){
  yield* ["a", "b", "c"];
}

gen().next() // { value:"a", done:false }
let read = (function* () {
  yield 'hello';
  yield* 'hello';
})();

read.next().value // "hello"
read.next().value // "h"

如果被代理的 Generator 函式有return語句,那麼就可以向代理它的 Generator 函式返回資料。

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返回的遍歷器物件。genFuncWithReturnreturn語句的返回值The result,會返回給函式logReturned內部的result變數,因此會有終端輸出

使用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']

作為物件屬性的 Generator 函式

如果一個物件的屬性是 Generator 函式,可以簡寫成下面的形式

let obj = {
  * myGeneratorMethod() {
    ···
  }
};

Generator 函式的 this

Generator函式總是返回一個遍歷器,ES6規定這個遍歷器是Generator函式的例項,也繼承了 Generator 函式的prototype物件上的方法。

如果把Generator當作普通的建構函式,並不會生效,因為其返回的總是遍歷器物件,而不是this物件。Generator 函式也不能跟new命令一起用,會報錯

function* g() {
  this.a = 11;
}

let obj = g();
obj.next();
obj.a // undefined

this物件繫結至Generator 函式的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

在外面包一層普通函式,即可使用new命令

function* F() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}

function F_constructor(){
    return F.call(F.prototype)
}

f = new F_constructor();

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

Generator 與上下文

JavaScript 程式碼執行時,會產生一個全域性的上下文環境(context,又稱執行環境),包含了當前所有的變數和物件。然後,執行函式(或塊級程式碼)的時候,又會在當前上下文環境的上層,產生一個函式執行的上下文,變成當前(active)的上下文,由此形成一個上下文環境的堆疊(context stack)。

這個堆疊是“後進先出”的資料結構,最後產生的上下文環境首先執行完成,退出堆疊,然後再執行完成它下層的上下文,直至所有程式碼執行完成,堆疊清空。

Generator 函式不是這樣,它執行產生的上下文環境,一旦遇到yield命令,就會暫時退出堆疊,但是並不消失,裡面的所有變數和物件會凍結在當前狀態。等到對它執行next命令時,這個上下文環境又會重新加入呼叫棧,凍結的變數和物件恢復執行。

應用

(1)非同步操作的同步化表示
Generator 函式的暫停執行的效果,意味著可以把非同步操作寫在yield表示式裡面,等到呼叫next方法時再往後執行。這實際上等同於不需要寫回調函數了,因為非同步操作的後續操作可以放在yield表示式下面,反正要等到呼叫next方法時再執行。所以,Generator函式的一個重要實際意義就是用來處理非同步操作,改寫回調函式。
下面是通過 Generator 函式部署 Ajax 操作

function* main() {
  var result = yield request("http://some.url");  //等待返回response
  var resp = JSON.parse(result);
    console.log(resp.value);
}

function request(url) {
  makeAjaxCall(url, function(response){
    it.next(response);   //通過next方法傳遞response
  });
}

var it = main();
it.next();

(2)控制流管理
利用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();
  }
}

將任務分解成步驟之後,還可以使用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);
  }
}

最後,就可以用for...of迴圈一次性依次執行所有任務的所有步驟

for (var step of iterateJobs(jobs)){
  console.log(step.id);
}

(3)部署Iterator介面
利用 Generator 函式,可以在任意物件上部署 Iterator 介面。

(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');
}

for (task of doStuff()) {
  // task是一個函式,可以像回撥函式那樣使用它
}