Generator 基礎指南

一個栗子

Generator函數是ES6提供的一種異步編程解決方案,它語法行爲與傳統函數不一樣,咱們先來看一個使用Generator書寫的Fibonacci函數的示例:es6

function* fibonacci(n) {
  let current = 0;
  let next = 1;
  while (n-- > 0) {
    yield current;
    [current, next] = [next, next + current];
  }
}
複製代碼

費波那契數列由0和1開始,以後的費波那契係數就是由以前的兩數相加而得出。而咱們上面的生成器參數n即表明須要計算到第幾個序列:編程

const test = fibonacci(3);

console.log(test.next()); // {value: 0, done: false}
console.log(test.next()); // {value: 1, done: false}
console.log(test.next()); // {value: 1, done: false}
console.log(test.next()); // {value: undefined, done: true}

for (item of fibonacci(3)) {
  console.log(item) // 0, 1, 1
}
複製代碼

經過以上實踐,能夠簡單總結出生成器的幾個特色:數組

  • 生成器函數在調用時不會當即執行,而是返回一個遍歷器對象。
  • 生成器中每一個yield表明一個新的狀態,經過遍歷器對象的next()方法執行並返回結果,返回對象結構爲:{value: any, done: boolean}
  • 遍歷器Iterator可由for ... of執行。

Generator生成器

Generator生成器與普通函數相比具備2個特徵:bash

  1. function與函數名之間有一個*
  2. 函數內部使用yield表達式。

當在調用生成器時,它不容許使用new關鍵字,也不會當即執行,而是返回一個Iterator對象,容許經過遍歷器、for...of、解構語法等表達式執行。數據結構

每個 yield 表達式,會使 Generator 生成一種新的狀態,並轉交控制權給外部函數,此時咱們須要調用遍歷器對象的next方法,才能使 Generator 繼續執行。須要注意的是,Generator函數內部若是不存在yield表達式,它也不會當即執行,而是須要手動使用next方法觸發:異步

function* test() { console.log('hello') }
const fn = test(); // 沒有任何輸出
fn.next(); // hello
複製代碼

Generator函數結束的標誌爲 return,return 返回的值也會做爲next方法返回對象的value,而此時 done 屬性爲:true(若是函數體內無 return 關鍵字,則會執行到函數結束,默認返回值爲undefined)。異步編程

yield表達式

yield表達式用於定義Generator不一樣的內部狀態,它同時做爲函數暫停的標誌,將執行權交給外部的其餘函數,並將 yield 關鍵字緊鄰的表達式做爲接下來遍歷器的next()方法返回的對象的value鍵值。外部函數在調用了next()方法之後,Generator才得以恢復執行:函數

function* test() {
  yield 'hello'
  yield 'world'
  return '!'
}

const executer = test();
executer.next(); // {value: "hello", done: false}
executer.next(); // {value: "world", done: false}
executer.next(); // {value: "!", done: true}


for (item of test()) { console.log(item) } // hello world

複製代碼

return與yield都能使函數中止執行,並將後面的表達式的值做爲返回對象value傳遞出去,區別在於return是函數結束的標識,不具有屢次執行的能力,返回的值也不能做爲迭代對象使用(迭代器若是判斷 done 標識爲true,則會忽略該值)。學習

那咱們能夠在生成器中調用生成器嗎?最開始嘗試時可能會這樣寫:ui

function* hello() {
    yield 'hello'
    yield world()
}

function* world() {
    yield 'world'
}

for (item of hello()) { console.log(item) }
// hello
// world {<suspended>}
複製代碼

第二個迭代器的值返回的是一個新的Generator,它按照原樣返回了,並無按照咱們預想中執行。爲了在一個Generator函數裏執行另外一個Generator,此時就須要使用yield*表達式:

function* hello() {
    yield 'hello'
    yield* world()
}

function* world() {
    yield 'world'
}

for (item of hello()) { console.log(item) }
// hello
// world
複製代碼

yield*語句後面能接生成器對象或是實現了Iterator接口的值(字符串對象、數組對象等),它的做用就像是將生成器對象進行了for...of遍歷,將每個遍歷到的對象傳遞到當前的生成器中執行yield,舉一個示例:

function* example() {
  yield 'hello';
  yield* ['world'];
  yield* test();
  yield* '??';
}

function* test() {
  yield '!';
}

// example函數等同於
function* example() {
  yield 'hello';
  for (item of ['world']) {
    yield item;
  }
  yield '!'
  for (item of '??') {
    yield item;
  }
}
複製代碼

Iterator遍歷器

經過以上對Generator函數的介紹,咱們對Iterator有了一個初步的瞭解,Generator函數在運行後,會生成一個遍歷器對象,再由for...of語法或是解構函數對Iterator進行消費。其實Iterator不只僅應用於Generator,它其實仍是一種通用的接口規範,爲不一樣的數據結構提供統一的訪問機制。

ES6規定,Iterator接口部署在對象的Symbol.iterator上,凡是實現了這一屬性的對象都認爲是可遍歷的(iterable),原生具有Iterator接口的數據結構有:

  • Array、String
  • Set、Map
  • TypedArray、NodeList

所以對一個字符串來說,咱們能夠手動獲取到它的遍歷器對象,並進行循環打印每個字符:

const strs = 'hello world'
for (str of strs[Symbol.iterator]()) { console.log(str) }
複製代碼

除以上原生實現了Iterable數據結構之外,咱們還能夠本身定義任意對象的Symbol.iterator屬性方法,從而實現Iterable特性,該屬性方法具有如下特徵:

  • 函數不須要任何參數,要求返回一個Iterator Object
  • Iterator Object中,存在一個next()方法,該函數老是返回一個{value: any, done: boolean}對象,done默認值爲falsevalue默認值爲undefined
  • value能夠是任意值
  • donetrue,表示迭代器已經執行到序列的末尾;donefalse表示迭代器還能夠繼續執行next()方法並返回下一個序列對象

實現Iterator接口有不少種方法,不論你的數據結構爲類仍是對象,咱們只要保證[Symbol.iterator]屬性方法及其返回的數據規範便可,如下爲自定義迭代器的示例:

// 使用生成器的方式,推薦使用
const obj  = {
  [Symbol.iterator] = function* () {
    yield 'hello';
    yield 'world';
  }
}

// 使用純函數的方式定義返回對象及next方法
const obj = {
  [Symbol.iterator]: () => {
    const items = ['hello', 'world'];
    let nextIndex = 0;
    return {
      next() {
        return nextIndex < items.length
          ? { value: items[nextIndex++] }
          : { done: true };
      }
    };
  }
};
for (item of obj) { console.log(item) }
複製代碼

next()、throw()與return()方法

這三種方法都屬於Generator的原型方法,經過其對象進行調用,目的是讓Generator恢復執行,並使用不一樣語句替換當前yield標誌所在的表達式:

  • next(value),將表達式替換爲value,next函數主要用於向生成器內部傳遞值,從而改變生成器的狀態。
  • throw(error),將表達式替換爲throw(error),throw方法會將Error對象交由生成器內部處理,若生成器沒法處理則會又將錯誤拋出來,此時會中斷生成器的執行。
  • return(value),將表達式替換爲return value,return方法用於中斷生成器的執行。

若要一一舉例,篇幅可能會很是長,所以在這裏舉一個包含這三種語句的示例:

function* test() {
  const flag = yield 'does anybody here';
  if (flag) {
    try {
      yield 'could you sing for me?';
    } catch (e) {
      if (e.message === 'sorry!') {
        yield 'I can teach you';
      } else {
        throw e
      }
    }
    yield 'maybe next time';
  }
}

const executer = test();
executer.next(); // {value: 'does anybody here?'}
executer.next(true); // { value: 'could you sing for me ?' }
executer.throw(new Error('sorry!')); // {value: 'i will teach you'}
executer.return('thank you'); // {value: "thank you", done: true}
複製代碼

函數有幾點須要解釋:

  • 當生成器小張詢問是否有人在這裏時,咱們須要經過next(true)進行迴應,以此來改變它的狀態。
  • 小張喜歡聽音樂,所以他會詢問你可否爲他唱首歌,假設你不會唱,你能夠經過throw拋出一個錯誤,說實在是抱歉,這時小張會熱情的教你唱歌;若是你的回答是「No Way!」,那麼小張就會當場崩潰。
  • 你贊成向小張學習並唱歌給他聽,能夠經過return方法向他表示感謝,此時對話就能夠終止了。

參考資料

相關文章
相關標籤/搜索