迭代器(Iterator)是一種設計模式(Design Pattern),可以讓程式開發人員依照一定順序來走訪某資料結構中的資料,還可以順便對資料進行一些運算。JavaScript在ES6之後內建了Symbol.iterator這個symbol型別的值,專門用在前面介紹過的for-of迴圈上。而同樣在ES6之後內建的產生器(Generator)則是用來快速產生迭代器的語法,它還額外提供了能夠從外部影響迭代器內部動作的功能。



迭代器(Iterator)

在TypeScript中,若要拿一個物件當作迭代器來用,則這個物件必須要提供next方法,且這個方法必須要回傳一個型別為{ done: boolean, value?: any }的物件。例如以下這個物件,就是一個迭代器:

const iterator = {
    next: () => (
        {
            done: false,
            value: 1,
        }
    ),
};

我們可以替任意物件加上Symbol.iterator屬性,這個Symbol.iterator屬性值必須要是一個會回傳迭代器物件的函數。例如:

const iterator = {
    next: () => (
        {
            done: false,
            value: 1,
        }
    ),
};

const o = {
    [Symbol.iterator]: () => {
        return iterator;
    },
};

以上這個o物件即為「可迭代」的物件,我們可以將其用在for-of的of關鍵字後。例如:

const iterator = {
    next: () => (
        {
            done: false,
            value: 1,
        }
    ),
};

const o = {
    [Symbol.iterator]: () => {
        return iterator;
    },
};

for (const e of o) {
    console.log(e);
}

以上程式,由於使用到Symbol.iterator這個在ES6之後才有的symbol型別的值,因此若要通過編譯,就要將TypeScript的編譯目標設為ES6以上的版本。編譯出來的JavaScript程式在執行之後會不斷地輸出1

不錯,這個for-of迴圈會根據我們的迭代器的next方法所回傳的型別為{ done: boolean, value?: any }的物件,來替變數e賦值。在每次迴圈開始迭代的時候,迭代器的next方法都會被呼叫,如果此時回傳的物件的done欄位為false,表示這個迴圈還沒有執行完,此時會將value欄位的值指派給of關鍵字前的常數或變數來儲存。

如果next方法回傳的物件的done欄位為true,表示這個迴圈已經執行完了,就不會再去管value欄位的值,直接跳出迴圈。

舉個比較複雜的例子吧!製作一個能夠讓for-of迴圈反向走訪陣列的迭代器。程式如下:

type RevIteratorResult<T> = { done: false, value: T } | { done: true, value: undefined };

class RevIterator<T> {
    private array: T[];

    private currentIndex = 0;

    constructor(array: T[]) {
        this.array = array;
        this.currentIndex = array.length;
    }

    next(): RevIteratorResult<T> {
        if (--this.currentIndex < 0) {
            return {
                done: true,
                value: undefined,
            };
        } else {
            return {
                done: false,
                value: this.array[this.currentIndex],
            };
        }
    }

    [Symbol.iterator](): RevIterator<T> {
        return this;
    }
}

const a = [1, 3, 5, 7, 9];

for (const e of new RevIterator(a)) {
    console.log(e);
}

以上程式會輸出:

9
7
5
3
1

這邊比較需要提起的幾個地方,首先是程式第27行,我們可以直接在類別中的方法名稱使用中括號[]來使用一個symbol型別的值作為屬性。再來是這個RevIterator類別因為同時有next方法和Symbol.iterator屬性,所以它本身就是「可被迭代的迭代器」,其實體可以直接被用在for-of迴圈的of關鍵字後。

不過事實上,以上並不是一個好的迭代器實作方式。我們應該要套用TypeScript內建的IterableIterator介面和IteratorResult型別來實作「可被迭代的迭代器」。

IterableIterator介面和IteratorResult型別的定義如下:

interface IteratorYieldResult<TYield> {
    done?: false;
    value: TYield;
}

interface IteratorReturnResult<TReturn> {
    done: true;
    value: TReturn;
}

type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>;

interface Iterator<T, TReturn = any, TNext = undefined> {
    next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
    return?(value?: TReturn): IteratorResult<T, TReturn>;
    throw?(e?: any): IteratorResult<T, TReturn>;
}

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

interface IterableIterator<T> extends Iterator<T> {
    [Symbol.iterator](): IterableIterator<T>;
}

先不要去管Iterator介面中的returnthrow屬性,這兩個是可選的,通常我們自行實作迭代器時並不會用到它們。在套用IterableIterator介面和IteratorResult型別之後,我們的程式變得如下:

class RevIterator<T> implements IterableIterator<T> {
    private array: T[];

    private currentIndex = 0;

    constructor(array: T[]) {
        this.array = array;
        this.currentIndex = array.length;
    }

    next(): IteratorResult<T> {
        if (--this.currentIndex < 0) {
            return {
                done: true,
                value: undefined,
            };
        } else {
            return {
                done: false,
                value: this.array[this.currentIndex],
            };
        }
    }

    [Symbol.iterator](): IterableIterator<T> {
        return this;
    }
}

const a = [1, 3, 5, 7, 9];

for (const e of new RevIterator(a)) {
    console.log(e);
}

程式第25行,其實也還是可以用RevIterator<T>作為函數回傳值的型別,因為RevIterator類別本身就實作IterableIterator介面。

產生器(Generator)

懶人迭代器

由於迭代器在實作上還是有一點麻煩,因此ES6之後還有提供產生器,可以在function關鍵字後緊接著一個星號*將任何的函數變成產生器函數來用,而產生器函數所回傳的值就是一個「迭代器」或「可被迭代的迭代器」。

如果TypeScript編譯目標是ES5之前的版本,就會在執行階段去判斷Symbol.iterator這個symbol型別的值是否可用(換句話說就算編譯目標是ES5之前的版本,也還是可以使用產生器)。不過若還要將「可被迭代的物件」用在for-of迴圈的話,還需要將downlevelIteration這個編譯設定項目設為true

舉例來說:

function* g() {
    return 1;
}

const iterator = g();

console.log(iterator.next());

以上程式會輸出:

{ value: 1, done: true }

在產生器函數中,我們可以用return關鍵字來回傳當done欄位值為true時的value欄位值。而done欄位值為false時的value欄位值則是用yield關鍵字來回傳的。

舉例來說:

function* g() {
    yield 1;
    yield 2;
    yield 3;

    return 4;
}

const iterator = g();

console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

以上程式會輸出:

{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: 4, done: true }
{ value: undefined, done: true }
{ value: undefined, done: true }

也就是說,呼叫產生器產生出來的「可被迭代的迭代器」的next方法,會去執行產生器函數主體中的程式碼。如果執行到yield關鍵字的敘述,就會將結果先返回並暫停執行產生器函數主體中的程式碼,直到下一次next方法被呼叫,才會從產生器函數主體中先前暫停的位置繼續執行程式。如果執行到return關鍵字的敘述,就會將最終結果回傳。此後再去呼叫next方法,得到的結果都會是{ value: undefined, done: true }

我們可以將剛才的反向迭代器用產生器來進行改寫,如下:

function* generateRevIterator<T>(array: T[]) {
    let currentIndex = array.length;

    while (--currentIndex >= 0) {
        yield array[currentIndex];
    }
}

const a = [1, 3, 5, 7, 9];

for (const e of generateRevIterator(a)) {
    console.log(e);
}

要成功編譯以上程式的話,tsconfig.json的設定檔可以是(但不限於)以下這樣:

{
  "compilerOptions": {
    ...

    "target": "ES6"

    ...
  }
  ...
}

{
  "compilerOptions": {
    ...

    "target": "ES5",
    "downlevelIteration": true

    ...
  }
  ...
}
從外部影響迭代器內部的動作
return

產生器函數回傳的迭代器會提供return方法(即Iterator介面定義的那個return屬性)。這個return方法可以控制迭代器立刻進入結束狀態,如果產生器主體中有使用return關鍵字來回傳資料的話,return方法就會有一個參數,其型別即為return關鍵字所回傳的值的型別,這個參數會被放入return方法回傳的物件的value屬性中。

舉例來說:

function* g() {
    console.log("Step: 1");
    yield 1;
    console.log("Step: 2");
    yield 2;
    console.log("Step: 3");
    return 3;
}

const iterator = g();

console.log(iterator.next());
console.log(iterator.return(0));
console.log(iterator.next());

以上程式會輸出:

Step: 1
{ value: 1, done: false }
{ value: 0, done: true }
{ value: undefined, done: true }
throw

產生器函數回傳的迭代器會提供throw方法(即Iterator介面定義的那個throw屬性)。這個throw方法可以控制迭代器在暫停點(yield關鍵字的位置)拋出錯誤訊息,如果迭代器已經迭代完成了(已回傳過done屬性為true的物件),則會在呼叫迭代器throw方法的位置拋出錯誤訊息。throw方法有一個參數,即為要拋出的錯誤訊息。

舉例來說:

function* g() {
    console.log("Step: 1");
    try {
        yield 1;
    } catch (err) {
        console.log(`Step 1 Error: ${err}`);
    }

    console.log("Step: 2");
    try {
        yield 2;
    } catch (err) {
        console.log(`Step 2 Error: ${err}`);
    }

    console.log("Step: 3");
    return 3;
}

const iterator = g();

console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.throw("22222"));
console.log(iterator.next());

try {
    console.log(iterator.throw("ttttt"));
} catch (err) {
    console.log(`throw: ${err}`);
}

以上程式會輸出:

Step: 1
{ value: 1, done: false }
Step: 2
{ value: 2, done: false }
Step 2 Error: 22222
Step: 3
{ value: 3, done: true }
{ value: undefined, done: true }
throw: ttttt
next

產生器函數回傳的迭代器會提供的next方法其實也可以影響迭代器內部的運作,傳進next方法作為第一個參數的值會被當下正在暫停、即將繼續執行的那個yield關鍵字給回傳。

舉例來說:

function* g() {
    console.log("Step: 1");
    const a = yield 1;
    console.log("Step: 2");
    yield a;
    console.log("Step: 3");
    return 3;
}

const iterator = g();

console.log(iterator.next(100));
console.log(iterator.next(200));
console.log(iterator.next(300));

以上程式會輸出:

Step: 1
{ value: 1, done: false }
Step: 2
{ value: 200, done: false }
Step: 3
{ value: 3, done: true }

程式第10行呼叫g這個產生器函數只會產生一個迭代器,並指派給iterator變數來儲存,並不會去執行產生器函數主體中的程式敘述。

程式第12行,第一次呼叫iteratornext方法,會去執行產生器函數主體中的程式敘述。執行到第3行時會遇到yield關鍵字,產生器函數主體中的程式敘述會暫停執行,然後將數值1回傳出來。

程式第13行,第二次呼叫iteratornext方法,並且將數值200代入其第一個參數,此時程式第三行,產生器函數主體中正在暫停的yield關鍵字會回傳數值200,然後指派給變數a來儲存,並繼續執行產生器函數主體中的程式敘述。執行到第5行時會遇到yield關鍵字,產生器函數主體中的程式敘述會暫停執行,然後將變數a儲存的值,也就是數值200回傳出來。

程式第14行,第三次呼叫iteratornext方法,並且將數值300代入其第一個參數,此時程式第五行,產生器函數主體中正在暫停的yield關鍵字會回傳數值300,但是並沒有指派給任何變數或常數來儲存,接著繼續執行產生器函數主體中的程式敘述。執行到第7行時會遇到return關鍵字,產生器函數主體中之後的程式敘述會停止執行,然後數值3會被回傳出來。

總結

在這個章節中,我們學會了在TypeScript中設計迭代器的方式,並且也學會透過產生器來快速產生「加強版」的迭代器。在接下來的三個章節中,要來搞懂JavaScript複雜的回呼(callback)、Promise和「async/await」語法。

下一章:事件循環(Event Loop)與回呼函數(Callback Function)