迭代器(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);
}
以上程式會輸出:
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
介面中的return
和throw
屬性,這兩個是可選的,通常我們自行實作迭代器時並不會用到它們。在套用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());
以上程式會輸出:
在產生器函數中,我們可以用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: 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());
以上程式會輸出:
{ 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}`);
}
以上程式會輸出:
{ 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));
以上程式會輸出:
{ value: 1, done: false }
Step: 2
{ value: 200, done: false }
Step: 3
{ value: 3, done: true }
程式第10行呼叫g
這個產生器函數只會產生一個迭代器,並指派給iterator
變數來儲存,並不會去執行產生器函數主體中的程式敘述。
程式第12行,第一次呼叫iterator
的next
方法,會去執行產生器函數主體中的程式敘述。執行到第3行時會遇到yield
關鍵字,產生器函數主體中的程式敘述會暫停執行,然後將數值1
回傳出來。
程式第13行,第二次呼叫iterator
的next
方法,並且將數值200
代入其第一個參數,此時程式第三行,產生器函數主體中正在暫停的yield
關鍵字會回傳數值200
,然後指派給變數a
來儲存,並繼續執行產生器函數主體中的程式敘述。執行到第5行時會遇到yield
關鍵字,產生器函數主體中的程式敘述會暫停執行,然後將變數a
儲存的值,也就是數值200
回傳出來。
程式第14行,第三次呼叫iterator
的next
方法,並且將數值300
代入其第一個參數,此時程式第五行,產生器函數主體中正在暫停的yield
關鍵字會回傳數值300
,但是並沒有指派給任何變數或常數來儲存,接著繼續執行產生器函數主體中的程式敘述。執行到第7行時會遇到return
關鍵字,產生器函數主體中之後的程式敘述會停止執行,然後數值3
會被回傳出來。
總結
在這個章節中,我們學會了在TypeScript中設計迭代器的方式,並且也學會透過產生器來快速產生「加強版」的迭代器。在接下來的三個章節中,要來搞懂JavaScript複雜的回呼(callback)、Promise
和「async/await」語法。
下一章:事件循環(Event Loop)與回呼函數(Callback Function)。