生成器與迭代器

以前的文章 寫到了 Generator 與異步編程的關係,其實簡化異步編程只是 Generator 的「副業」,Generator 自己卻不是爲異步編程而存在。git

生成器函數

咱們看 Generator 自身的含義——生成器,就是產生序列用的。好比有以下函數:github

function* range(start, stop) {
  for (let item = start; item < stop; ++item) {
    yield item;
  }
}

range 就是一個生成器函數,它自身是函數能夠調用(typeof range === 'function' // true),但又與普通函數不一樣,生成器函數(GeneratorFunction)永遠返回一個生成器(Generator編程

注:咱們一般所說的 Generator 實際上指生成器函數(GeneratorFunction),而把生成器函數返回的對象稱做迭代器(Iterator)。因爲感受「生成器函數」返回「生成器」這句話有些拗口,下文沿用生成器和迭代器的說法。segmentfault

迭代器

初次調用生成器實際上不執行生成器函數的函數體,它只是返回一個迭代器,當用戶調用迭代器的 next 函數時,程序纔開始真正執行生成器的函數體。當程序運行到 yield 表達式時,會將 yield 後面表達式的值做爲 next 函數的返回值(的一部分)返回,函數自己暫停執行。數組

const iterator = range(0, 10); // 獲取迭代器
const value1 = iterator.next().value; // 獲取第一個值 => 0
const value2 = iterator.next().value; // 獲取第二個值 => 1

next 返回值是一個對象,包含兩個屬性 valuedone。value 即爲 yield 後面表達式的值,done 表示函數是否已經結束(return)。若是函數 return(或執行到函數尾退出,至關於 return undefined),則 done 爲 true,value 爲 return 的值。瀏覽器

for...of 是遍歷整個迭代器的簡單方式。異步

生成器的用處

上面說到,生成器就是生成序列用的。可是與直接返回數組不一樣,生成器返回序列是一項一項計算並返回的,而返回數組老是須要計算出全部值後統一返回。因此至少有三種狀況應該考慮使用生成器。異步編程

  • 序列有無限多項,或者調用者不肯定須要多少項

range(0, Infinity) 是容許的,由於生成器沒生成一個值就會暫停執行,因此不會形成死循環,能夠由調用者選擇什麼時候中止。函數

注意此時不能使用 for...of,由於迭代器永遠不會 donethis

  • 計算每一項耗時較長

若是計算一項的值須要 1ms,那麼計算 1000 項就須要 1s,若是不將這 1s 拆分,就會致使瀏覽器卡頓甚至假死。這時能夠用生成器每生成幾項就將控制權交還給瀏覽器,用於響應用戶事件,提高用戶體驗(固然這裏更有效的方法是將代碼放到 Web Worker 裏執行)

  • 節省內存

若是序列很長,直接返回數組會佔用較大內存。生成器返回值是一項一項返回,不會一次性佔用大量內存(固然生成器爲了保留執行上下文比一般函數佔用內存更多,可是它是個定值,不隨迭代次數增長)

使用生成器實現懶加載的 map、filter

Array#mapArray#filter 是 ES5 引入的(絕對不算新的)兩個很是經常使用的函數,前者將數組每一項經過回調函數映射到新數組(值變量不變),後者經過回調函數過濾某些不須要的項(量變值不變),他們都會生成新的數組對象,若是數組自己較長或者寫了很長的 map、filter 調用鏈,就可能形成內存浪費。

這時就能夠考慮使用生成器實現這兩個函數,很是簡單:

function* map(iterable, callback) {
  let i = 0;
  for (const item of iterable) {         // 遍歷數組
    yield callback(item, i++, iterable); // 獲取其中一項,調用回調函數,yield 返回值
  }
}

function* filter(iterable, callback) {
  let i = 0;
  for (const item of iterable) {         // 遍歷數組
    if (callback(item, i++, iterable)) { // 獲取其中一項,調用回調函數
      yield item;                        // 僅當回調函數返回真時,才 yield 值
    }
  }
}

能夠看到我在代碼中寫的是「可迭代的」(iterable),而不限於數組(因爲實現了 Symbol.iterator 因此數組也是可迭代對象)。好比能夠這麼用:

const result = map(     // (1)
  filter(               // (2)
    range(1, 10000),    // (3)
    x => x % 2 === 0,
  ),
  x => x / 2,
)
console.log(...result); // (4)

注意,程序在解構運算符 ...result 這一步才真正開始計算 result 的值(所謂的懶加載),並且它的值也是一個一個計算的:

  1. (3)生成迭代器,提供值給(2);(2)提供值給(1)
  2. (1)中的result也是迭代器,在這一步全部函數都沒有真正開始執行,由於沒有任何代碼問他們索要值。
  3. (4)中的擴展運算符對迭代器 result 進行了求值,生成器真正開始執行。
  4. result 的值來自於 (1),因而(1)首先開始執行。
  5. (1)中map函數使用 for...of 遍歷(2)提供的迭代器,因而(2)開始執行
  6. (2)中filter函數使用 for...of 遍歷(3)提供的迭代器,因而(3)開始執行
  7. (3)中range函數開始執行,循環獲得第一個值 1。遇到 yield 關鍵字,將值 1 輸出給(2)
  8. (2)中的 for...of 得到一個值 1,執行函數體。callback 返回 false,忽略之。回到 for...of,繼續問(3)索要下一個值
  9. (3)range 輸出第二個值 2
  10. (2)中的 for...of 得到一個值 2,執行函數體。callback 返回 true,將值 2 輸出給 (1)
  11. (1)中的 for...of 得到一個值 2,執行函數體獲得 1。將值 1 輸出給(4),console.log 得到第一個參數
  12. (4)檢測result尚未結束(done爲false),問result索要下一個值。
  13. 回到第 4 步循環,直至(3)中的循環結束函數體退出爲止,(3)返回的迭代器被關閉
  14. (2)中 for...of 檢測到迭代器已被關閉(done爲true),循環結束,函數退出,(2)返回的迭代器被關閉
  15. 同理(1)返回的迭代器被關閉
  16. (4)中解構運算符檢測到result已關閉,結構結束。將結構獲得的全部值做爲 console.log 的參數列表輸出

總結一下,代碼執行順序大概是這樣:(3) -> (2) -> (1) -> (4) -> (1) -> (2) -> (3) -> (2) -> (3) -> (2) -> (1) -> (4) -> (1) -> (2) -> (3) -> ……

是否是至關複雜?異步函數中「跳來跳去」的執行順序也就是這麼來的。跟遞歸函數同樣,不要太糾結生成器函數的執行順序,而要着重理解它這一步究竟要作什麼事情。

生成器函數的鏈式調用

這樣的代碼 map(filter(range(1, 100), x => x % 2 === 0), x => x / 2) 彷佛有些d疼,好像又看到了被回調函數支配的恐懼。雖然有人提出了管道符的提議,但這種 stage 1 的提議被標準接受直至有瀏覽器實現實在是遙遙無期,有沒有其餘辦法呢?

普通函數能夠簡單的經過在類中返回 this 實現函數的鏈式調用(例如經典的 jQuery),可是這點在生成器中不適用。咱們前面說過生成器函數自己永遠返回一個迭代器,而生成器中的 return 語句其實是關閉迭代器的標誌,return this 實際表明 { value: this, done: true }。生成器中的 return 和普通函數用法相近但實際含義大大不一樣。

鏈式調用須要函數的返回值是個對象,而且對象中包含可鏈式調用的全部函數。生成器函數返回的迭代器自己就是一個對象,很容易想到改變對象的原型實現。

迭代器有以下原型繼承鏈:

迭代器對象 -> 生成器.prototype -> 生成器.prototype.prototype(Generator) -> Object.prototype -> null

clipboard.png

能夠看到,生成器返回的迭代器對象就好像是被生成器 new 出來的同樣(可是生成器不是構造函數不能被 new)。可是總之咱們能夠經過給生成器函數的 prototype 添加方法實現給迭代器添加方法的效果。實現以下

function* range(start, stop) {
  for (let item = start; item < stop; ++item) {
    yield item;
  }
}

function* map(callback) {
  let i = 0;
  for (const item of this) {
    yield callback(item, i++, this);
  }
}

function* filter(callback) {
  let i = 0;
  for (const item of this) {
    if (callback(item, i++, this)) {
      yield item;
    }
  }
}

[range, map, filter].forEach(x => Object.assign(x.prototype, { range, map, filter }));

// 使用
const result = range(1, 100)
  .filter(x => x % 2 === 0)
  .map(x => x / 2);

console.log(...result);

筆者業(xian)餘(de)時(dan)間(teng)使用迭代器實現了幾乎全部 ES7 中 Array 的成員方法和靜態方法,廣而告之,歡迎來噴:https://github.com/CarterLi/q...

相關文章
相關標籤/搜索