JavaScript ES6 Iterables/Iterators 迭代器

JavaScript 有好幾種集合 (collection) 類型的資料結構,像是 Array, Object, Map, Set,ES6 介紹了統一的機制來遍歷 (iteration) 這些不同的數據結構,這機制叫做 iterator。

Iterator 是一種接口 (Protocol) 的定義,你可以把接口看作是一種實作的規則,只要一個物件符合 Iterator protocol 定義的規則,我們就可以稱這個物件是一個 iterator (迭代器)。

如果一個物件實作了 Iterator protocol,就等於:

  1. 定義一個物件將如何被遍歷
  2. 讓資料結構中的成員,可以按照你控制的順序來做排列

在 JavaScript 中,一個 Iterator 只是一個有提供 next() 方法的物件。

next() 方法:

  1. 用來負責依序返回物件中所有可被遍歷的成員
  2. 每一次 next() 被調用時,會返回一個有 donevalue 屬性的物件 ,done 是一個 boolean,如果是 true 表示遍歷結束,value 則是當前遍歷到的成員的值

所以一個 Iterator 被遍歷的過程像是這樣子:

  1. Iterator 本質上像是一個指針 (pointer),維護一個位置紀錄 (internal state),用來記錄目前遍歷到哪一個成員
  2. 第一次調用 next() 時,會返回資料結構中的第一個成員,同時更新 pointer 紀錄
  3. 第二次調用 next() 時,則返回第二個成員,同時更新 pointer 紀錄
  4. 遍歷過程會不斷的調用 next(),直到最後一個成員
  5. 再一次調用 next() 時,返回 {done: true} 表示結束

範例:

function makeIterator(array) {
    // 記錄目前的位置
    var nextIndex = 0;
    
    return {
       next: function() {
           return nextIndex < array.length ?
               {value: array[nextIndex++], done: false} :
               {done: true};
       }
    };
}

上面定義了一個 makeIterator 函數,用來生成一個 Iterator:

var it = makeIterator(['yo', 'ya']);

// 顯示 yo
console.log(it.next().value);

// 顯示 ya
console.log(it.next().value);

// 顯示 true
console.log(it.next().done);

從上面例子你可以發現,一個物件會怎麼被遍歷是你可以全權控制和定義的!

那怎麼知道一個物件是可以被遍歷的 - 也就是可以被 for...of

當一個物件被 for...of 遍歷時,JavaScript 首先需要取得一個 Iterator。一個物件只要定義了如何取得 Iterator 的接口,我們就稱這個物件是可以被遍歷的 (iterable)

定義如何取得一個物件的 Iterator 的規則,就是所謂的 Iterable Protocol。

Iterable Protocol 定義了,如果一個物件要能被遍歷,必須實作 Symbol.iterator 這個 Symbol 物件屬性,Symbol.iterator 的值是一個函數,用來取得一個 iterator。

有些內建的資料型態,像是 Array, Map 已經先定義好 Symbol.iterator 接口,所以才可以被 for...of。

來看看如何實作 iterable 接口:

var foo = {
    [Symbol.iterator]: () => ({
        items: ['p', 'o', 'n', 'y', 'f', 'o', 'o'],
        next: function next () {
            return {
                done: this.items.length === 0,
                value: this.items.shift()
            }
        }
    })
}

// foo 物件實作了 Iterable 接口,所以它可以被遍歷

for (let pony of foo) {
    console.log(pony)
}

// 上面會依序輸出
// p
// o
// n
// y
// f
// o
// o

Object 之所以沒有預設的 Symbol.iterator 接口,是因為屬性被遍歷的先後順序是不確定的,需要你自己去定義。

最後再來複習一下 Iterator 和 Iterable 的接口規則:

interface Iterable {
    [Symbol.iterator](): Iterator;
}

interface Iterator {
    next(): IteratorResult;
}

interface IteratorResult {
    value: any;
    done: boolean;
}