[前端怪談_1] 從 for of 聊到 Generator

dendoink發表於2019-03-02

你能學到什麼

  • for of 更深入的理解
  • iterator 到底是何方神聖?
  • 陣列也是物件,為什麼不能用 for of 來遍歷物件呢?
  • 如何實現物件的 for of?
  • Generator 又是何方神聖?
  • Generator 有什麼用呢?

聊聊for of

說起 for of 相信每個寫過 JavaScript 的人都用過 for of ,平時我們用它做什麼呢?大多數情況應該就是遍歷陣列了,當然,更多時候,我們也會用 map() 或者 filer() 來遍歷一個陣列。 但是就像我們標題裡面說的,它跟 Generator 能扯上什麼關係呢?

首先我們想一個問題,為什麼使用 for of 或者 map() / filer() 方法就可以遍歷一個陣列 (或者類陣列物件: Strings , Maps , Sets , arguments ) 呢? 為什麼不能用他們來遍歷一個物件呢?

在真正揭開謎底之前,站在 for of 的角度想一下,現在讓你去遍歷一個陣列,你需要知道什麼資訊呢?

  • 對應下標的值
  • 是否遍歷結束的標誌

帶著這樣的思考,我們列印一個陣列來看看這裡面的玄機:

const numbersArray = [1, 2, 3];

console.dir(numbersArray);
複製程式碼
[前端怪談_1] 從 for of 聊到 Generator

陣列 (或者類陣列物件: Strings , Maps , Sets , arguments ) 的原型中都實現了一個方法 Symbol.iterator,問題來了,那麼這個 Symbol.iterator 又有什麼用呢? 拿出來試一下就知道了:

let iterator = numbersArray[Symbol.iterator]();
// 我們把這個 Symbol.iterator 列印一下看看裡面到底有些什麼
console.dir(iterator);
複製程式碼
[前端怪談_1] 從 for of 聊到 Generator

這裡有一個 next() 方法對嗎?執行這個 next() 方法:

iterator.next(); // 輸出 {value: 1, done: false}
iterator.next(); // 輸出 {value: 2, done: false}
iterator.next(); // 輸出 {value: 3, done: false}
iterator.next(); // 輸出 {value: undefined, done: true}
複製程式碼

請注意,當下標超出時,value: undefined

我們發現這個 iterator.next() 每次都返回了一個物件。這物件包含兩個資訊:當前下標的值,以及遍歷是否結束的標誌。這印證了我們之前思考,有了這兩個資訊,你作為 for of 函式,也能列印出陣列的每一項了不是嗎?

新的問題來了,iterator 到底是何方神聖呢?

iterator(迭代器) & The iterator protocol(迭代協議)

聊到了 iterator 我們不得不先說一下 The iterator protocol(迭代協議)

” The iterable protocol allows JavaScript objects to define or customize their iteration behavior ” – MDN

MDN 上是這麼說的:The iterator protocol 允許 JavaScript 物件去定義或定製它們的迭代行為 ,所以上面出現的 Symbol.iterator 這個方法,就是陣列對於這個協議的實現。那麼按照這個協議,陣列是怎麼實現了一個 iterator 呢?

“In JavaScript an iterator is an object which defines a sequence and potentially a return value upon its termination. More specifically an iterator is any object which implements the Iterator protocol by having a next() method which returns an object with two properties: value, the next value in the sequence; and done, which is true if the last value in the sequence has already been consumed. If value is present alongside done, it is the iterator`s return value.” – MDN

這一大段看起來比較費勁,簡單來說就像我們上一章節所印證的,它實現的方式是定義了一個 next() 方法,而這個 next() 方法每次被執行都會返回一個物件: {value:xxx/undefined , done: true/false } 其中 value 代表的是當前遍歷到的值,done 代表是否遍歷結束。

本小節回答了我們之前的提問: 為什麼不能用 for of 來遍歷一個物件呢? 原因很簡單:JavaScript 的物件中沒有實現一個這樣的 iterator 。你可以列印一個物件來看看結果如何:

console.dir({ a: 1, b: 2 });
複製程式碼
[前端怪談_1] 從 for of 聊到 Generator

okay, 到這裡如果就結束的話,那我們瞭解得還不夠深入,於是再問一個問題:

Why is there no built-in object iteration ? (為什麼在 object 中沒有內建迭代器呢? )

為什麼在 object 中沒有內建迭代器呢?

對啊,為什麼呢? 我們在各樣的場景中也需要來遍歷一個物件啊?為什麼沒有內建一個迭代器呢?要回答這個問題,我們得從另外一個角度出發,瞭解一些基本的概念:

我們常常說遍歷物件,但是簡單來說,只會在兩種層級上來對一個 JavaScript 物件進行遍歷:

  • 程式的層級 – 什麼意思呢?在程式層級上,我們對一個物件進行迭代,是在迭代展示其結構的物件屬性。 可能還不是很好理解,舉個例子:Array.prototype.length 這個屬性與物件的結構相關,但卻不是它的資料。

  • 資料的層級 – 意味著迭代資料結構並提取它的資料。舉個例子:我們在迭代一個陣列的時候,迭代器是對於它的 每一個資料進行迭代,如果 array = [a, b, c, d] 那麼迭代器訪問到的是 1, 2, 3, 4

明白了這個緣由,JavaScript 雖然不支援用 for of 來遍歷物件,但是提供了一個 for in 方法來遍歷所有非 Symbol 型別並且是可列舉的屬性。

標準不支援,如果我們就是要用 for-of 來遍歷物件呢?那我們可以任性的實現一個啊:

Object.prototype[Symbol.iterator] = function*() {
  for (const [key, value] of Object.entries(this)) {
    yield { key, value };
  }
};
複製程式碼
for (const { key, value } of { a: 1, b: 2, c: 3 }) {
  console.log(key, value);
}
複製程式碼

不知道你有沒有注意一個細節,在我們任性的實現一個 iterator 的程式碼中,我們用到了一個很奇怪的結構 function*() {} ,這個就是我們接下來要介紹的 Generator

Generators

看到這個名字覺得很厲害哈,但其實很簡單,寫一個 Generator 你只需要在函式名和 function 關鍵字中間加一個 * 號就可以了。至於裡面的 yield 是什麼,後面會說的。

talk is cheap , show me the code ,用一個例子,簡單說一下概念。

我們現在定義了一個這樣的 Generator 叫做 gen

function* gen() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
}
複製程式碼

我們只能看到,這裡面有 4 個語句,那列印一下看看唄:

[前端怪談_1] 從 for of 聊到 Generator

這裡發現了一個熟悉的函式,next() 方法,我們把 gen 例項化一下,執行一下它的 next() 來看看結果:

[前端怪談_1] 從 for of 聊到 Generator

還是熟悉的味道,那麼到這裡,我們已經知道,Generator 可以例項化出一個 iterator ,並且這個 yield 語句就是用來中斷程式碼的執行的,也就是說,配合 next() 方法,每次只會執行一個 yield 語句。

多說一句,針對 Generator 本身,還有一個有意思的特性,yield 後面可以跟上另一個 Generator 並且他們會按照次序執行:

function* gen() {
  yield 1;
  yield* gen2();
  return;
}

function* gen2() {
  yield 4;
  yield 5;
}

let iterator = gen();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
複製程式碼
[前端怪談_1] 從 for of 聊到 Generator

結果很有意思不是嗎?而且 return 會終結整個 Generator ,換句話說:寫在 return 後面的 yield 不會執行。

Generator 有什麼用?

Generator 有什麼用? 聰明的同學可能已經猜到了,是的,它能夠中斷執行程式碼的特性,可以幫助我們來控制非同步程式碼的執行順序:

例如有兩個非同步的函式 AB, 並且 B 的引數是 A 的返回值,也就是說,如果 A 沒有執行結束,我們不能執行 B

那這時候我們寫一段虛擬碼:

function* effect() {
  const { param } = yield A();
  const { result } = yield B(param);
  console.table(result);
}
複製程式碼

這時候我們如果需要得到 result 那麼我們就需要:

const iterator = effect()
iterator.next()
iterator.next()
複製程式碼

執行兩次 next() 得到結果,看起來很傻不是嗎?有沒有好的辦法呢?(廢話,肯定有啊)
假設你在每次執行 A() / B() 的請求結束之後,都會自動執行 next() 方法呢?這不就解決了嗎?

這樣的庫早就存在了,建議大家參考 co 的原始碼,當然你也可以通過閱讀 這篇文章 來看看,到底 Generator 是怎麼玩的。

相關文章