ES6系列--7. 可迭代協議和迭代器協議

Escape Plan發表於2018-07-02

ECMAScript 2015的幾個補充,並不是新的內建或語法,而是協議。這些協議可以被任何遵循某些約定的物件來實現。 有兩個協議:可迭代協議和迭代器協議。

可迭代協議

可迭代協議允許 JavaScript 物件去定義或定製它們的迭代行為, 例如(定義)在一個 for..of 結構中什麼值可以被迴圈(得到)。一些內建型別都是內建的可迭代物件並且有預設的迭代行為, 比如 Array or Map, 另一些型別則不是 (比如Object) 。

Iterator 介面的目的,就是為所有資料結構,提供了一種統一的訪問機制,即for...of迴圈(詳見下文)。當使用for...of迴圈遍歷某種資料結構時,該迴圈會自動去尋找 Iterator 介面,呼叫Symbol.iterator方法,返回該物件的預設遍歷器。

ES6 規定,預設的 Iterator 介面部署在資料結構的Symbol.iterator屬性,或者說,一個資料結構只要具有Symbol.iterator屬性,就可以認為是“可迭代的”(iterable)。Symbol.iterator屬性本身是一個函式,就是當前資料結構預設的遍歷器生成函式。執行這個函式,就會返回一個遍歷器。

為了變成可迭代物件, 一個物件必須實現(或者它原型鏈的某個物件)必須有一個名字是 Symbol.iterator 的屬性:

迭代器協議

該迭代器協議定義了一種標準的方式來產生一個有限或無限序列的值。

JavaScript 原有的表示“集合”的資料結構,主要是陣列(Array)和物件(Object),ES6 又新增了MapSet。這樣就有了四種資料集合,使用者還可以組合使用它們,定義自己的資料結構,比如陣列的成員是MapMap的成員是物件。這樣就需要一種統一的介面機制,來處理所有不同的資料結構。

迭代器(Iterator)就是這樣一種機制。它是一種介面,為各種不同的資料結構提供統一的訪問機制。任何資料結構只要部署 Iterator 介面,就可以完成遍歷操作(即依次處理該資料結構的所有成員)。

Iterator 的作用有三個:一是為各種資料結構,提供一個統一的、簡便的訪問介面;二是使得資料結構的成員能夠按某種次序排列;三是 ES6創造了一種新的遍歷命令for...of迴圈,Iterator 介面主要供for...of消費。

Iterator 的遍歷過程是這樣的。

  1. 建立一個指標物件,指向當前資料結構的起始位置。也就是說,遍歷器物件本質上,就是一個指標物件。

  2. 第一次呼叫指標物件的next方法,可以將指標指向資料結構的第一個成員。

  3. 第二次呼叫指標物件的next方法,指標就指向資料結構的第二個成員。

  4. 不斷呼叫指標物件的next方法,直到它指向資料結構的結束位置。

每一次呼叫next方法,都會返回資料結構的當前成員的資訊。具體來說,就是返回一個包含valuedone兩個屬性的物件。其中,value屬性是當前成員的值,done屬性是一個布林值,表示遍歷是否結束。

var someString = "hi";
typeof someString[Symbol.iterator]; // "function"

var iterator = someString[Symbol.iterator]();
iterator + "";    // "[object String Iterator]"

iterator.next()       // { value: "h", done: false }
iterator.next();      // { value: "i", done: false }
iterator.next();      // { value: undefined, done: true }
複製程式碼

原生具備 Iterator 介面的資料結構如下。

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函式的 arguments 物件
  • NodeList 物件

注意物件是不具備 Iterator 介面的,一個物件如果要具備可被for...of迴圈呼叫的 Iterator 介面,就必須在Symbol.iterator的屬性上部署遍歷器生成方法(原型鏈上的物件具有該方法也可)。

呼叫 Iterator 介面的場合

有一些場合會預設呼叫 Iterator 介面(即Symbol.iterator方法),除了下文會介紹的for...of迴圈,解構賦值, 擴充套件運算子其實也會呼叫預設的Iterator介面。

實際上,這提供了一種簡便機制,可以將任何部署了 Iterator 介面的資料結構,轉為陣列。也就是說,只要某個資料結構部署了 Iterator 介面,就可以對它使用擴充套件運算子,將其轉為陣列。

由於陣列的遍歷會呼叫遍歷器介面,所以任何接受陣列作為引數的場合,其實都呼叫了遍歷器介面。下面是一些例子。

  • for...of
  • Array.from()
  • Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]]))
  • Promise.all()
  • Promise.race()

for...of

for...of 迴圈是最新新增到 JavaScript 迴圈系列中的迴圈。

它結合了其兄弟迴圈形式 for 迴圈和 for...in 迴圈的優勢,可以迴圈任何可迭代(也就是遵守可迭代協議)型別的資料。預設情況下,包含以下資料型別:String、Array、MapSet,注意不包含 Object 資料型別(即 {})。預設情況下,物件不可迭代。

在研究 for...of 迴圈之前,先快速瞭解下其他 for 迴圈,看看它們有哪些不足之處。

for 迴圈

for 迴圈的最大缺點是需要跟蹤計數器和退出條件。我們使用變數 i 作為計數器來跟蹤迴圈並訪問陣列中的值。我們還使用 Array.length 來判斷迴圈的退出條件。

雖然 for 迴圈在迴圈陣列時的確具有優勢,但是某些資料結構不是陣列,因此並非始終適合使用 loop 迴圈。

for...in 迴圈

for...in 迴圈改善了 for 迴圈的不足之處,它消除了計數器邏輯和退出條件。但是依然需要使用 index 來訪問陣列的值.

此外,當你需要向陣列中新增額外的方法(或另一個物件)時,for...in 迴圈會帶來很大的麻煩。因為 for...in 迴圈迴圈訪問所有可列舉的屬性,意味著如果向陣列的原型中新增任何其他屬性,這些屬性也會出現在迴圈中。這就是為何在迴圈訪問陣列時,不建議使用 for...in 迴圈。

注意: forEach 迴圈 是另一種形式的 JavaScript 迴圈。但是,forEach() 實際上是陣列方法,因此只能用在陣列中。也無法停止或退出 forEach 迴圈。如果希望你的迴圈中出現這種行為,則需要使用基本的 for 迴圈。

for...of 迴圈

for...of 迴圈用於迴圈訪問任何可迭代的資料型別。

const digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

for (const digit of digits) {
  console.log(digit);
}
複製程式碼

可以隨時停止或退出 for...of 迴圈。

const digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

for (const digit of digits) {
  if (digit % 2 === 0) {
    continue;
  }
  console.log(digit); //1,3,5,7,9
}
複製程式碼

不用擔心向物件中新增新的屬性。for...of 迴圈將只迴圈訪問物件中的值。

相關文章