Generator函數的語法, yield表達式, next方法理解

Generator 函數的語法

1.0 簡介

Generator 函數是 ES6 提供的一種異步編程解決方案,語法行爲與傳統函數完全不同.

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

形式上,Generator 函數是一個普通函數,但是有兩個特徵。一是,function關鍵字與函數名之間有一個星號;二是,函數體內部使用yield表達式,定義不同的內部狀態(yield在英語裏的意思就是“產出”)。

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

var hw = helloWorldGenerator();

Generator 函數的調用方法與普通函數一樣,也是在函數名後面加上一對圓括號。不同的是,調用 Generator 函數後,該函數並不執行,返回的也不是函數運行結果,而是一個指向內部狀態的指針對象

下一步,必須調用遍歷器對象的next方法,使得指針移向下一個狀態。也就是說,每次調用next方法,內部指針就從函數頭部或上一次停下來的地方開始執行,直到遇到下一個yield表達式(或return語句)爲止。換言之,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 }

2.0 yield表達式

由於 Generator 函數返回的遍歷器對象,只有調用next方法纔會遍歷下一個內部狀態,所以其實提供了一種可以暫停執行的函數。yield表達式就是暫停標誌

遍歷器對象的next方法的運行邏輯如下。

  1. 遇到 yield 表達式, 就暫停執行後面的操作, 並將緊跟在 yield 後面的那個表達式的值, 作爲返回的對象的 value 的屬性值.
  2. 下一次調用 next 方法時, 再繼續向下執行, 直到遇到下一個 yield 表達式再次暫停, 並獲取到 yield 之後的值.
  3. 如果沒有再遇到 新的 yield 表達式, 就一直運行到函數結束, 直到 return 語句爲止, 並將return 語句後面的表達式的值, 作爲返回對象的 value 的屬性值.
  4. 如果該函數沒有 return 語句, 則返回對象的 value 的屬性值爲 undefined

需要注意的是,yield表達式後面的表達式,只有當調用next方法、內部指針指向該語句時纔會執行,因此等於爲 JavaScript 提供了手動的“惰性求值”(Lazy Evaluation)的語法功能。

function* findGenerator() {
    const first = yield 2 //
    console.log('first ===>>>', first); // first爲 undefined說明  yield 後面的值 直接返回給了外部調用next()方法的指針對象
    yield 3
}
ge = findGenerator()
console.log(ge.next()); // {value: 2, done: false} 然後暫停, 函數內部的 console語句不再執行.

console.log(ge.next()); // first ===>>> undefined  {value: 3, done: false}
// 第二次調用next方法, 打印了函數體內的打印語句, 和 yield 3 之後的值, 說明yield 2處被暫停, 再調用next() 方法之後, 它之後的代碼才執行


yield表達式與return語句既有相似之處,也有區別

  • 相似之處在於,都能返回緊跟在語句後面的那個表達式的值
  • 區別在於每次遇到yield,函數暫停執行,下一次再從該位置繼續向後執行,而return語句不具備位置記憶的功能 一個函數裏面,只能執行一次(或者說一個)return語句,但是可以執行多次(或者說多個)yield表達式。
  • 正常函數只能返回一個值,因爲只能執行一次return;Generator 函數可以返回一系列的值,因爲可以有任意多個yield。從另一個角度看,也可以說 Generator 生成了一系列的值,這也就是它的名稱的來歷(英語中,generator 這個詞是“生成器”的意思)。

Generator 函數可以不用 yield 表達式, 這時就變成了一個單純的暫緩執行函數.

function* f() {
    console.log('執行了!')
}

var generator = f();

setTimeout(function () {
    generator.next() // 只有調用 next() 方法 generator 函數 f 內部纔會執行
}, 500);

上面代碼中,函數f如果是普通函數,在爲變量generator賦值時就會執行,

但是,函數f是一個 Generator 函數,就變成只有調用next方法時,函數f纔會執行。

另外需要注意,yield表達式只能用在 Generator 函數裏面,用在其他地方都會報錯。

3.0 next 方法的參數

yield 表達式, 本身沒有返回值, 或者說總是返回 undefined , next 方法可以帶一個參數, 該參數就會被當做上一個 yield 表達式的放回值.

function * nextParamGenerator() {
    yield 2
    const param1 =yield 3
    console.log(param1); // next函數傳入參數, 會作爲上一個 yield 表達式的返回值
    yield 4
}
const gen = nextParamGenerator()
console.log(gen.next()); // {value: 2, done: false}
console.log(gen.next()); // {value: 3, done: false}
console.log(gen.next('next函數傳入參數, 會作爲上一個 yield 表達式的返回值'));

4.0 for … of 循環

for...of循環可以自動遍歷 Generator 函數運行時生成的Iterator對象,且此時不再需要調用next方法。

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

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

上面代碼使用for...of循環,依次顯示 5 個yield表達式的值。這裏需要注意,一旦next方法的返回對象的done屬性爲truefor...of循環就會中止,且不包含該返回對象,所以上面代碼的return語句返回的6,不包括在for...of循環之中。

5.0 Generator.prototype.throw()

Generator 函數返回的遍歷器對象,都有一個throw方法,可以在函數體外拋出錯誤,然後在 Generator 函數體內捕獲。

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

上面代碼中,遍歷器對象i連續拋出兩個錯誤。第一個錯誤被 Generator 函數體內的catch語句捕獲。i第二次拋出錯誤,由於 Generator 函數內部的catch語句已經執行過了,不會再捕捉到這個錯誤了,所以這個錯誤就被拋出了 Generator 函數體,被函數體外的catch語句捕獲。

throw方法可以接受一個參數,該參數會被catch語句接收,建議拋出Error對象的實例

var g = function* () {
  try {
    yield;
  } catch (e) {
    console.log(e);
  }
};

var i = g();
i.next();
i.throw(new Error('出錯了!'));
// Error: 出錯了!(…)

6.0 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

7.0 next(), throw(), return()的共同點

next()throw()return()這三個方法本質上是同一件事,可以放在一起理解。它們的作用都是讓 Generator 函數恢復執行,並且使用不同的語句替換yield表達式。

next()是將yield表達式替換成一個值。

const g = function* (x, y) {
  let result = yield x + y;
  return result;
};

const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}

gen.next(1); // Object {value: 1, done: true}
// 相當於將 let result = yield x + y
// 替換成 let result = 1;

上面代碼中,第二個next(1)方法就相當於將yield表達式替換成一個值1。如果next方法沒有參數,就相當於替換成undefined

throw()是將yield表達式替換成一個throw語句。

gen.throw(new Error('出錯了')); // Uncaught Error: 出錯了
// 相當於將 let result = yield x + y
// 替換成 let result = throw(new Error('出錯了'));

return()是將yield表達式替換成一個return語句

gen.return(2); // Object {value: 2, done: true}
// 相當於將 let result = yield x + y
// 替換成 let result = return 2;

8.0 yield* 表達式

如果在 Generator 函數內部,調用另一個 Generator 函數。需要在前者的函數體內部,自己手動完成遍歷。

function* foo() {
  yield 'a';
  yield 'b';
}

function* bar() {
  yield 'x';
  // 手動遍歷 foo()
  for (let i of foo()) {
    console.log(i);
  }
  yield 'y';
}

for (let v of bar()){
  console.log(v);
}
// x
// a
// b
// y

上面代碼中,foobar都是 Generator 函數,在bar裏面調用foo,就需要手動遍歷foo。如果有多個 Generator 函數嵌套,寫起來就非常麻煩。

ES6 提供了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"

9.0 作爲對象屬性的 Generator 函數

如果一個對象的屬性是 Generator 函數, 可以簡寫成下面的兩種格式

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

上面代碼中,myGeneratorMethod屬性前面有一個星號,表示這個屬性是一個 Generator 函數。

它的完整形式如下,與上面的寫法是等價的。

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

10.0 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.next();
obj.a // undefined

Generator 函數也不能跟new命令一起用,會報錯。

= g();

obj instanceof g // true
obj.hello() // ‘hi!’


上面代碼表明,Generator 函數`g`返回的遍歷器`obj`,是`g`的實例,而且繼承了`g.prototype`。但是,如果把`g`當作普通的構造函數,並不會生效,因爲`g`返回的總是遍歷器對象,而不是`this`對象。

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

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

Generator 函數也不能跟new命令一起用,會報錯。

總結:

上一章: Promise對象及相關實例方法介紹
下一章: Generator 函數異步應用大概介紹

交流學習添加微信(備註技術交流學習): Gene199302
在這裏插入圖片描述

該博客爲學習阮一峯 ES6入門課所做的筆記記錄, 僅用來留作筆記記錄和學習理解

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章