迭代協議與生成器 101

moZLeo發表於2019-10-11

Intro

首先關於這些名稱可能很多人都很模糊不清,儘管都是有關迭代遍歷但並不清楚彼此的定義和區別,那現在我們就慢慢的解釋清楚。

terminology

那我們優先從每一個術語稱呼開始吧,來看看這些術語對應的中文名:

  • iterable object: [可]迭代物件

  • Iteration protocols: 迭代協議

  • generator:生成器

Iteration protocols - 迭代協議

細心的同學大概已經發現 Iteration protocols 是複數。是的在協議裡其實有2種協議分別是:

  • Iteration protocols

    • iterator protocol:迭代器協議

    • iterable protocol:可迭代協議

what are they?

在以往的JavaScripts中已經存在許多對集合型別對迭代方法,例如:for .. in, for 迴圈, map(), forEach()... 這些語法或者API都是有JavaScript內部實現如何進行迭代集合。例如:

  1. for .. in語法就是遍歷所有non-Symbol的可列舉屬性
  2. map()API 就是對陣列對索引依次遍歷得到一個新陣列;

那麼我們如何對一個變數實現我們自定義的迭代方式呢,這就需要依靠迭代協議。其實在我看來 迭代器協議 與 可迭代協議 是非常類似的以至於初探時後很難弄清,下面我們分別來看看2個協議。

iterator protocol - 迭代器協議

image
???

其實 迭代器協議 就是一個物件上定義了一個next屬性,而這個next屬性的定義有一定的要求,滿足的話這就是一個實現了迭代器協議的物件,也被叫做 iterator : 迭代器

那麼這個next屬性有宣告要求呢?

  1. 首先 next 是一個方法,它不接受任何引數傳入;
  2. 其次呼叫next這個方法會返回一個物件,它包含2個屬性 value & done,其中 value代表本次迭代得到的資料而 done用來表示迭代是否結束。例如:
    image
    以上 iterator 變數就是一個實現了迭代器協議的迭代器物件。

✌️PS:迭代器協議只是一種協議它指定了一個物件的迭代行為控制(每次迭代返回值、迭代終點),但如何自動迭代執行還需要自己編碼實現(不然你得完全編碼不停的iterator.next()、iterator.next()、iterator.next()),且每次迭代的上下文你得自己想辦法保留。

iterable protocol - 可迭代協議

image
???

其實可迭代協議是一個物件是擁有 @@iterator 屬性,而這個屬性鍵的定義來自 Symbol.iterator, 同樣@@iterator 屬性有一定要求,滿足要求就實現了可迭代協議。

這些要求分別是:

  1. [Symbol.iterator](key name)屬性是一個方法,且不接受任何引數;
  2. 方法返回一個物件,這個物件就是迭代器協議物件。例如:
    image

這就是兩種迭代協議對內容與區別,那麼說完迭代協議我們可以來談談iterable object。

iterable object - [可]迭代物件

可迭代物件是物件上實現了 iterable protocol - 可迭代協議 的物件,且可以使用build-ins語法進行迭代,例如 for (let i in iterable)[...iterable]。 ⚠️注意: 使用這些build-ins語法必須是物件上實現了可迭代協議不是迭代器協議,否則對物件迭代將會丟擲異常:

❌ Uncaught TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))
    at <anonymous>:1:1
複製程式碼

目前有很多JavaScript內建對資料集合已經實現了迭代器協議,有:

  1. Array
const iterable = [10, 20, 30];

for (const value of iterable) {
  console.log(value); // 10 20 30
}
複製程式碼
  1. String
const iterable = 'boo';

for (const value of iterable) {
  console.log(value);  // 'b' 'o' 'o'
}
複製程式碼
  1. TypedArray
  2. Map & Set
  3. fn arguments
  4. DOM collections

到此你可能發現了要實現自定義迭代行為在寫法上還是很複雜對,並且這裡存在兩種迭代協議,不同的庫或者工具可能選用某一種方法實現迭代物件的行為,那麼就會可能造成不相容。但由於2中協議期本質又是十分類似所有我們可以創造一個同時滿足迭代器協議和可迭代協議的物件,它類似:

var myIterator = {
    next: function() {
        // ...
    },
    [Symbol.iterator]: function() { return this }
}
複製程式碼

這樣看起來還是很複雜,於是有了我們最後要說的 generator

generator - 生成器

generator物件generator函式 返回,它既符合[可]迭代協議,又符合迭代器協議,就像剛剛那種模版寫法。

它的寫法如下:

// 生成器函式
function* gen() { 
  yield 1;
  yield 2;
}

// 生成器物件
const g = gen();
複製程式碼

JavaScript支援了生成器語法我們就可以更快的實現自定義的迭代物件了,例如上面的一個例子我用生成器實現是這樣的:

image

具體的 generator 語法再次不再過多解釋,這就是 generator 與 itera... 之間的關係。

⚠️注意: 生成器物件不要重複使用 這句話什麼意思,我們先來看一個MDN例子?:

const gen = (function *(){
  yield 1;
  yield 2;
  yield 3;
})();
for (const o of gen) {
  console.log(o);
  break;  // Closes iterator
}

// The generator should not be re-used, the following does not make sense!
for (const o of gen) {
  console.log(o); // Never called.
}
複製程式碼

以上程式碼我們可以知道 generator物件 就像是一個一次性消費品(一次性筷子?)被迭代行為操作一次後將不會再次進行迭代。


基本上所有的東西就說完了,在補充說明最後一點東東

  1. 有了自定義迭代那麼如何實現 迭代流 實現 非阻塞程式碼呢?在很早以前TJ大佬有實現一個庫CO就是幹這件事情的該庫在社群也比較流行。
  2. 迭代器是一個很好去寫非同步程式碼的方式,但在 es2017 async/await 語法糖的引入,是的非同步程式碼的編寫與閱讀更加方便。

相關文章