21. 可迭代和迭代器
目錄
請支持這本書:購買它(PDF、EPUB、MOBI)捐款
(廣告,請不要封鎖。)

21. 可迭代和迭代器



21.1 概述

ES6 引進一個新的機制來遍歷資料:迭代。兩個概念是迭代的核心

以 TypeScript 符號表示的介面,這些角色看起來像這樣

interface Iterable {
    [Symbol.iterator]() : Iterator;
}
interface Iterator {
    next() : IteratorResult;
}
interface IteratorResult {
    value: any;
    done: boolean;
}

21.1.1 可迭代值

下列值可迭代

一般物件不可迭代(原因說明於 專門章節)。

21.1.2 支援迭代的建構

透過迭代存取資料的語言建構

21.2 可迭代性

可迭代性的概念如下。

讓每個消費者都支援所有來源並不實際,特別是因為應該可以建立新的來源(例如透過函式庫)。因此,ES6 引入了 Iterable 介面。資料消費者使用它,資料來源實作它

由於 JavaScript 沒有介面,Iterable 比較像慣例

讓我們看看陣列 arr 的消耗情況。首先,您透過其金鑰為 Symbol.iterator 的方法建立一個迭代器

> const arr = ['a', 'b', 'c'];
> const iter = arr[Symbol.iterator]();

然後,您重複呼叫迭代器的 next() 方法,以擷取陣列「內部」的項目

> iter.next()
{ value: 'a', done: false }
> iter.next()
{ value: 'b', done: false }
> iter.next()
{ value: 'c', done: false }
> iter.next()
{ value: undefined, done: true }

如您所見,next() 會傳回每個項目包裝在一個物件中,作為 value 屬性的值。布林屬性 done 表示項目順序的結尾何時到達。

Iterable 和迭代器是所謂協定介面加上使用它們的規則)的一部分,用於迭代。此協定的主要特性是它具有順序性:迭代器一次傳回一個值。這表示如果可迭代資料結構是非線性的(例如樹),則迭代會將其線性化。

21.3 可迭代資料來源

我將使用 for-of 迴圈(請參閱章節「for-of 迴圈」)來反覆運算各種可迭代資料。

21.3.1 陣列

陣列(和型別化陣列)可反覆運算其元素

for (const x of ['a', 'b']) {
    console.log(x);
}
// Output:
// 'a'
// 'b'

21.3.2 字串

字串可迭代,但它們會反覆運算 Unicode 編碼點,每個編碼點可能包含一個或兩個 JavaScript 字元

for (const x of 'a\uD83D\uDC0A') {
    console.log(x);
}
// Output:
// 'a'
// '\uD83D\uDC0A' (crocodile emoji)

21.3.3 映射

映射可反覆運算其條目。每個條目編碼為 [金鑰、值] 對,一個具有兩個元素的陣列。條目總是會以確定性的方式反覆運算,順序與其加入映射的順序相同。

const map = new Map().set('a', 1).set('b', 2);
for (const pair of map) {
    console.log(pair);
}
// Output:
// ['a', 1]
// ['b', 2]

請注意,弱映射不可迭代。

21.3.4 集合

集合可反覆運算其元素(反覆運算的順序與其加入集合的順序相同)。

const set = new Set().add('a').add('b');
for (const x of set) {
    console.log(x);
}
// Output:
// 'a'
// 'b'

請注意,弱集合不可迭代。

21.3.5 arguments

儘管特殊變數 arguments 在 ECMAScript 6 中或多或少已過時(由於 rest 參數),但它仍可迭代

function printArgs() {
    for (const x of arguments) {
        console.log(x);
    }
}
printArgs('a', 'b');

// Output:
// 'a'
// 'b'

21.3.6 DOM 資料結構

大多數 DOM 資料結構最終都會是可迭代的

for (const node of document.querySelectorAll('div')) {
    ···
}

請注意,實作此功能的工作仍在進行中。但這相對容易做到,因為符號 Symbol.iterator 無法與現有屬性金鑰衝突。

21.3.7 可迭代計算資料

並非所有可迭代內容都必須來自資料結構,它也可以即時計算。例如,所有主要的 ES6 資料結構(陣列、型別化陣列、映射、集合)都有三種方法會傳回可迭代物件

讓我們看看它看起來像什麼。entries() 提供了一個很好的方法來取得陣列元素及其索引

const arr = ['a', 'b', 'c'];
for (const pair of arr.entries()) {
    console.log(pair);
}
// Output:
// [0, 'a']
// [1, 'b']
// [2, 'c']

21.3.8 純粹物件不可迭代

純粹物件(由物件字面值建立)不可迭代

for (const x of {}) { // TypeError
    console.log(x);
}

為什麼物件預設不可迭代屬性?原因如下。在 JavaScript 中,你可以迭代兩個層級

  1. 程式層級:迭代屬性表示檢查程式的結構。
  2. 資料層級:迭代資料結構表示檢查程式管理的資料。

預設讓迭代屬性會混淆這些層級,這會有兩個缺點

如果引擎要透過方法 Object.prototype[Symbol.iterator]() 實作可迭代性,那麼會有一個額外的警告:透過 Object.create(null) 建立的物件將不可迭代,因為 Object.prototype 不在它們的原型鏈中。

重要的是要記住,迭代物件的屬性主要是有趣的,如果你使用物件作為 Maps1。但我們只在 ES5 中這樣做,因為我們沒有更好的替代方案。在 ECMAScript 6 中,我們有內建的資料結構 Map

21.3.8.1 如何迭代屬性

迭代屬性的正確(且安全的)方法是透過工具函式。例如,透過 objectEntries()其實作稍後顯示(未來的 ECMAScript 版本可能會內建類似功能)

const obj = { first: 'Jane', last: 'Doe' };

for (const [key,value] of objectEntries(obj)) {
    console.log(`${key}: ${value}`);
}

// Output:
// first: Jane
// last: Doe

21.4 迭代語言建構

以下 ES6 語言建構使用迭代協定

以下各節詳細說明每一個建構。

21.4.1 透過陣列範本解構

透過陣列範本解構適用於任何可迭代物件

const set = new Set().add('a').add('b').add('c');

const [x,y] = set;
    // x='a'; y='b'

const [first, ...rest] = set;
    // first='a'; rest=['b','c'];

21.4.2 for-of 迴圈

for-of 是 ECMAScript 6 中的新迴圈。其基本形式如下所示

for (const x of iterable) {
    ···
}

如需更多資訊,請參閱章節「for-of 迴圈」。

請注意,需要 iterable 的可迭代性,否則 for-of 無法對值進行迴圈。這表示必須將不可迭代值轉換為可迭代值。例如,透過 Array.from()

21.4.3 Array.from()

Array.from() 將可迭代值和類似陣列的值轉換為陣列。它也可用於類型化陣列。

> Array.from(new Map().set(false, 'no').set(true, 'yes'))
[[false,'no'], [true,'yes']]
> Array.from({ length: 2, 0: 'hello', 1: 'world' })
['hello', 'world']

如需有關 Array.from() 的更多資訊,請參閱陣列章節

21.4.4 散佈運算子 (...)

散佈運算子將可迭代值插入陣列

> const arr = ['b', 'c'];
> ['a', ...arr, 'd']
['a', 'b', 'c', 'd']

這表示它提供一種簡潔的方式將任何可迭代值轉換為陣列

const arr = [...iterable];

散佈運算子也會將可迭代值轉換為函式、方法或建構式呼叫的引數

> Math.max(...[-1, 8, 3])
8

21.4.5 Map 和 Set

Map 的建構式將可迭代的 [key, value] 成對轉換為 Map

> const map = new Map([['uno', 'one'], ['dos', 'two']]);
> map.get('uno')
'one'
> map.get('dos')
'two'

Set 的建構式將可迭代的元素轉換為 Set

> const set = new Set(['red', 'green', 'blue']);
> set.has('red')
true
> set.has('yellow')
false

WeakMapWeakSet 的建構式以類似的方式運作。此外,Map 和 Set 本身是可迭代的(WeakMap 和 WeakSet 則否),這表示您可以使用其建構式複製它們。

21.4.6 Promise

Promise.all()Promise.race() 接受可迭代的 Promise

Promise.all(iterableOverPromises).then(···);
Promise.race(iterableOverPromises).then(···);

21.4.7 yield*

yield* 是一個僅在產生器內可用的運算子。它會產生可迭代物件迭代的所有項目。

function* yieldAllValuesOf(iterable) {
    yield* iterable;
}

yield* 最重要的使用案例是遞迴呼叫產生器(產生可迭代物件)。

21.5 實作可迭代物件

在本節中,我將詳細說明如何實作可迭代物件。請注意,ES6 產生器 通常比「手動」執行此任務方便得多。

迭代協定如下所示。

如果物件有一個金鑰為 Symbol.iterator 的方法(自有或繼承),則該物件會變成可迭代(「實作」Iterable 介面)。該方法必須傳回一個迭代器,一個透過其方法 next() 迭代可迭代物件「內部」項目 的物件。

在 TypeScript 表示法中,可迭代物件和迭代器的介面如下所示2

interface Iterable {
    [Symbol.iterator]() : Iterator;
}
interface Iterator {
    next() : IteratorResult;
    return?(value? : any) : IteratorResult;
}
interface IteratorResult {
    value: any;
    done: boolean;
}

return() 是我們稍後會用到的選用方法3。讓我們先實作一個虛擬可迭代物件來了解迭代運作的方式。

const iterable = {
    [Symbol.iterator]() {
        let step = 0;
        const iterator = {
            next() {
                if (step <= 2) {
                    step++;
                }
                switch (step) {
                    case 1:
                        return { value: 'hello', done: false };
                    case 2:
                        return { value: 'world', done: false };
                    default:
                        return { value: undefined, done: true };
                }
            }
        };
        return iterator;
    }
};

讓我們檢查 iterable 是否真的是可迭代的

for (const x of iterable) {
    console.log(x);
}
// Output:
// hello
// world

這段程式碼執行三個步驟,其中計數器 step 確保所有事情都按順序發生。首先,我們傳回值 'hello',然後傳回值 'world',最後我們指出已到達迭代的結尾。每個項目都包覆在一個具有下列屬性的物件中

如果 donefalse,則可以省略 done;如果 valueundefined,則可以省略 value。也就是說,switch 陳述式可以寫成如下所示。

switch (step) {
    case 1:
        return { value: 'hello' };
    case 2:
        return { value: 'world' };
    default:
        return { done: true };
}

產生器章節中所說明,有些情況下,你會希望即使是最後一個項目 done: true 也有 value。否則,next() 可以更簡單,並直接傳回項目(不將它們包覆在物件中)。然後,將透過特殊值(例如符號)指出迭代的結尾。

我們來看一個可迭代物件的另一個實作。函式 iterateOver() 傳回一個可迭代物件,迭代傳遞給它的引數

function iterateOver(...args) {
    let index = 0;
    const iterable = {
        [Symbol.iterator]() {
            const iterator = {
                next() {
                    if (index < args.length) {
                        return { value: args[index++] };
                    } else {
                        return { done: true };
                    }
                }
            };
            return iterator;
        }
    }
    return iterable;
}

// Using `iterateOver()`:
for (const x of iterateOver('fee', 'fi', 'fo', 'fum')) {
    console.log(x);
}

// Output:
// fee
// fi
// fo
// fum

21.5.1 可迭代的迭代器

如果可迭代物件和迭代器是同一個物件,則前一個函式可以簡化

function iterateOver(...args) {
    let index = 0;
    const iterable = {
        [Symbol.iterator]() {
            return this;
        },
        next() {
            if (index < args.length) {
                return { value: args[index++] };
            } else {
                return { done: true };
            }
        },
    };
    return iterable;
}

即使原始的可迭代物件和迭代器不是同一個物件,如果迭代器有下列方法(這也讓它成為可迭代物件),偶爾還是很有用的

[Symbol.iterator]() {
    return this;
}

所有內建的 ES6 迭代器都遵循此模式(透過一個共用原型,請參閱生成器的章節)。例如,陣列的預設迭代器

> const arr = [];
> const iterator = arr[Symbol.iterator]();
> iterator[Symbol.iterator]() === iterator
true

為什麼迭代器同時也是可迭代物件是有用的?for-of 僅適用於可迭代物件,不適用於迭代器。由於陣列迭代器是可迭代的,因此您可以在另一個迴圈中繼續反覆運算

const arr = ['a', 'b'];
const iterator = arr[Symbol.iterator]();

for (const x of iterator) {
    console.log(x); // a
    break;
}

// Continue with same iterator:
for (const x of iterator) {
    console.log(x); // b
}

繼續反覆運算的一個用例是,您可以在透過 for-of 處理實際內容之前移除初始項目(例如標頭)。

21.5.2 可選的迭代器方法:return()throw()

兩個迭代器方法是可選的

21.5.2.1 透過 return() 關閉迭代器

如前所述,可選的迭代器方法 return() 是關於讓迭代器在未反覆運算到結尾時進行清理。它會關閉迭代器。在 for-of 迴圈中,提早(或在規格語言中為突然)終止可能是由下列原因造成的

在這些情況的每一個情況中,for-of 會讓迭代器知道迴圈不會完成。我們來看一個範例,一個函式 readLinesSync,它會傳回檔案中文字行的可迭代物件,並希望不論發生什麼事都關閉該檔案

function readLinesSync(fileName) {
    const file = ···;
    return {
        ···
        next() {
            if (file.isAtEndOfFile()) {
                file.close();
                return { done: true };
            }
            ···
        },
        return() {
            file.close();
            return { done: true };
        },
    };
}

由於 return(),檔案會在下列迴圈中適當地關閉

// Only print first line
for (const line of readLinesSync(fileName)) {
    console.log(x);
    break;
}

return() 方法必須傳回一個物件。這是因為產生器如何處理 return 陳述式,並會在生成器的章節中說明。

下列結構會關閉未完全「耗盡」的迭代器

後面的章節有更多關於關閉迭代器的資訊。

21.6 更多可迭代物件的範例

在本章節中,我們將探討更多可迭代物件的範例。這些可迭代物件大多數都可以透過產生器更輕鬆地實作。關於產生器的章節將會說明如何實作。

21.6.1 傳回可迭代物件的工具函式

傳回可迭代物件的工具函式和方法與可迭代資料結構一樣重要。以下是用於遍歷物件自有屬性的工具函式。

function objectEntries(obj) {
    let index = 0;

    // In ES6, you can use strings or symbols as property keys,
    // Reflect.ownKeys() retrieves both
    const propKeys = Reflect.ownKeys(obj);

    return {
        [Symbol.iterator]() {
            return this;
        },
        next() {
            if (index < propKeys.length) {
                const key = propKeys[index];
                index++;
                return { value: [key, obj[key]] };
            } else {
                return { done: true };
            }
        }
    };
}

const obj = { first: 'Jane', last: 'Doe' };
for (const [key,value] of objectEntries(obj)) {
    console.log(`${key}: ${value}`);
}

// Output:
// first: Jane
// last: Doe

另一個選項是使用迭代器而不是索引來遍歷具有屬性金鑰的陣列

function objectEntries(obj) {
    let iter = Reflect.ownKeys(obj)[Symbol.iterator]();

    return {
        [Symbol.iterator]() {
            return this;
        },
        next() {
            let { done, value: key } = iter.next();
            if (done) {
                return { done: true };
            }
            return { value: [key, obj[key]] };
        }
    };
}

21.6.2 可迭代物件的組合器

組合器4是將現有的可迭代物件組合起來以建立新的可迭代物件的函式。

21.6.2.1 take(n, iterable)

讓我們從組合器函式 take(n, iterable) 開始,它會傳回 iterablen 個項目的可迭代物件。

function take(n, iterable) {
    const iter = iterable[Symbol.iterator]();
    return {
        [Symbol.iterator]() {
            return this;
        },
        next() {
            if (n > 0) {
                n--;
                return iter.next();
            } else {
                return { done: true };
            }
        }
    };
}
const arr = ['a', 'b', 'c', 'd'];
for (const x of take(2, arr)) {
    console.log(x);
}
// Output:
// a
// b
21.6.2.2 zip(...iterables)

zipn 個可迭代物件轉換成 n 個元組的可迭代物件(編碼為長度為 n 的陣列)。

function zip(...iterables) {
    const iterators = iterables.map(i => i[Symbol.iterator]());
    let done = false;
    return {
        [Symbol.iterator]() {
            return this;
        },
        next() {
            if (!done) {
                const items = iterators.map(i => i.next());
                done = items.some(item => item.done);
                if (!done) {
                    return { value: items.map(i => i.value) };
                }
                // Done for the first time: close all iterators
                for (const iterator of iterators) {
                    if (typeof iterator.return === 'function') {
                        iterator.return();
                    }
                }
            }
            // We are done
            return { done: true };
        }
    }
}

正如你所見,最短的可迭代物件會決定結果的長度

const zipped = zip(['a', 'b', 'c'], ['d', 'e', 'f', 'g']);
for (const x of zipped) {
    console.log(x);
}
// Output:
// ['a', 'd']
// ['b', 'e']
// ['c', 'f']

21.6.3 無限可迭代物件

有些可迭代物件可能永遠不會 done

function naturalNumbers() {
    let n = 0;
    return {
        [Symbol.iterator]() {
            return this;
        },
        next() {
            return { value: n++ };
        }
    }
}

對於無限可迭代物件,你不能遍歷它的「所有」項目。例如,透過中斷 for-of 迴圈

for (const x of naturalNumbers()) {
    if (x > 2) break;
    console.log(x);
}

或只存取無限可迭代物件的開頭

const [a, b, c] = naturalNumbers();
    // a=0; b=1; c=2;

或使用組合器。take() 是一個選項

for (const x of take(3, naturalNumbers())) {
    console.log(x);
}
// Output:
// 0
// 1
// 2

zip() 傳回的可迭代物件的「長度」是由其最短的輸入可迭代物件決定的。這表示 zip()naturalNumbers() 可以讓你對任意(有限)長度的可迭代物件進行編號

const zipped = zip(['a', 'b', 'c'], naturalNumbers());
for (const x of zipped) {
    console.log(x);
}
// Output:
// ['a', 0]
// ['b', 1]
// ['c', 2]

21.7 常見問答:可迭代物件和迭代器

21.7.1 迭代協定不會很慢嗎?

你可能會擔心迭代協定很慢,因為每次呼叫 next() 都會建立一個新的物件。然而,現代引擎中小型物件的記憶體管理很快,而且從長遠來看,引擎可以最佳化迭代,這樣就不需要配置中間物件。在es-discuss 上的討論串有更多資訊。

21.7.2 我可以重複使用同一個物件好幾次嗎?

原則上,沒有任何因素會阻止迭代器重複使用相同的迭代結果物件好幾次,我預期大多數情況都能順利運作。不過,如果客戶端快取迭代結果,就會出現問題

const iterationResults = [];
const iterator = iterable[Symbol.iterator]();
let iterationResult;
while (!(iterationResult = iterator.next()).done) {
    iterationResults.push(iterationResult);
}

如果迭代器重複使用其迭代結果物件,iterationResults 通常會包含同一個物件好幾次。

21.7.3 為什麼 ECMAScript 6 沒有可迭代組合器?

您可能會好奇為什麼 ECMAScript 6 沒有可迭代組合器,也就是用於處理可迭代物件或建立可迭代物件的工具。這是因為計畫分兩步驟進行

最後,會將其中一個函式庫或來自多個函式庫的程式碼片段新增到 JavaScript 標準函式庫。

如果您想了解這種函式庫可能長什麼樣子,請查看標準 Python 模組 itertools

21.7.4 可迭代物件是不是很難實作?

是的,可迭代物件很難實作,如果您手動實作的話。 下一章將介紹產生器,它有助於執行這項任務(以及其他事情)。

21.8 深入探討 ECMAScript 6 迭代協定

迭代協定包含下列介面(我已從 Iterator 遺漏 throw(),它僅受 yield* 支援,而且在其中是選用的)

interface Iterable {
    [Symbol.iterator]() : Iterator;
}
interface Iterator {
    next() : IteratorResult;
    return?(value? : any) : IteratorResult;
}
interface IteratorResult {
    value : any;
    done : boolean;
}

21.8.1 迭代

next() 的規則

21.8.1.1 IteratorResult

迭代結果的 done 屬性不一定要是 truefalse,真值或假值就夠了。所有內建語言機制都讓您可以省略 done: false

21.8.1.2 傳回新迭代器的可迭代物件與始終傳回相同迭代器的可迭代物件

有些可迭代物件每次被要求時都會產生一個新的反覆器。例如,陣列

function getIterator(iterable) {
    return iterable[Symbol.iterator]();
}

const iterable = ['a', 'b'];
console.log(getIterator(iterable) === getIterator(iterable)); // false

其他可迭代物件每次都會傳回同一個反覆器。例如,產生器物件

function* elements() {
    yield 'a';
    yield 'b';
}
const iterable = elements();
console.log(getIterator(iterable) === getIterator(iterable)); // true

當您多次反覆運算同一個可迭代物件時,可迭代物件是否產生新的反覆器很重要。例如,透過下列函數

function iterateTwice(iterable) {
    for (const x of iterable) {
        console.log(x);
    }
    for (const x of iterable) {
        console.log(x);
    }
}

使用新的反覆器,您可以多次反覆運算同一個可迭代物件

iterateTwice(['a', 'b']);
// Output:
// a
// b
// a
// b

如果每次都傳回同一個反覆器,您就無法這麼做

iterateTwice(elements());
// Output:
// a
// b

請注意,標準函式庫中的每個反覆器也是一個可迭代物件。它的方法 [Symbol.iterator]() 傳回 this,表示它總是傳回同一個反覆器(本身)。

21.8.2 關閉反覆器

反覆運算協定區分結束反覆器的兩種方式

呼叫 return() 的規則

實作 return() 的規則

以下程式碼說明,如果在收到 done 迭代器結果之前中斷 for-of 迴圈,則會呼叫 return()。也就是說,即使在收到最後一個值後中斷,也會呼叫 return()。這很微妙,當您手動迭代或實作迭代器時,必須小心處理才能正確無誤。

function createIterable() {
    let done = false;
    const iterable = {
        [Symbol.iterator]() {
            return this;
        },
        next() {
            if (!done) {
                done = true;
                return { done: false, value: 'a' };
            } else {
                return { done: true, value: undefined };
            }
        },
        return() {
            console.log('return() was called!');
        },
    };
    return iterable;
}
for (const x of createIterable()) {
    console.log(x);
    // There is only one value in the iterable and
    // we abort the loop after receiving it
    break;
}
// Output:
// a
// return() was called!
21.8.2.1 可關閉的迭代器

如果迭代器具有 return() 方法,則表示該迭代器為可關閉的。並非所有迭代器都可關閉。例如,陣列迭代器即不可關閉

> let iterable = ['a', 'b', 'c'];
> const iterator = iterable[Symbol.iterator]();
> 'return' in iterator
false

預設情況下,產生器物件可關閉。例如,以下產生器函式傳回的物件

function* elements() {
    yield 'a';
    yield 'b';
    yield 'c';
}

如果您在 elements() 的結果上呼叫 return(),則迭代會結束

> const iterator = elements();
> iterator.next()
{ value: 'a', done: false }
> iterator.return()
{ value: undefined, done: true }
> iterator.next()
{ value: undefined, done: true }

如果迭代器不可關閉,則在從 for-of 迴圈異常結束(例如 A 行中的結束)後,您仍可繼續迭代

function twoLoops(iterator) {
    for (const x of iterator) {
        console.log(x);
        break; // (A)
    }
    for (const x of iterator) {
        console.log(x);
    }
}
function getIterator(iterable) {
    return iterable[Symbol.iterator]();
}

twoLoops(getIterator(['a', 'b', 'c']));
// Output:
// a
// b
// c

相反地,elements() 傳回可關閉的迭代器,而 twoLoops() 內部的第二個迴圈沒有任何內容可供迭代

twoLoops(elements());
// Output:
// a
21.8.2.2 防止關閉迭代器

以下類別是防止關閉迭代器的通用解決方案。它透過包裝迭代器並轉送 return() 以外的所有方法呼叫來執行此操作。

class PreventReturn {
    constructor(iterator) {
        this.iterator = iterator;
    }
    /** Must also be iterable, so that for-of works */
    [Symbol.iterator]() {
        return this;
    }
    next() {
        return this.iterator.next();
    }
    return(value = undefined) {
        return { done: false, value };
    }
    // Not relevant for iterators: `throw()`
}

如果我們使用 PreventReturn,則在 twoLoops() 的第一個迴圈中異常結束後,產生器 elements() 的結果不會關閉。

function* elements() {
    yield 'a';
    yield 'b';
    yield 'c';
}
function twoLoops(iterator) {
    for (const x of iterator) {
        console.log(x);
        break; // abrupt exit
    }
    for (const x of iterator) {
        console.log(x);
    }
}
twoLoops(elements());
// Output:
// a

twoLoops(new PreventReturn(elements()));
// Output:
// a
// b
// c

還有另一種方法可以讓產生器無法關閉:產生器函式 elements() 產生的所有產生器物件都具有原型物件 elements.prototype。您可以透過 elements.prototype 隱藏 return() 的預設實作(位於 elements.prototype 的原型中),如下所示

// Make generator object unclosable
// Warning: may not work in transpilers
elements.prototype.return = undefined;

twoLoops(elements());
// Output:
// a
// b
// c
21.8.2.3 透過 try-finally 處理產生器中的清除

有些產生器需要在完成迭代後清除(釋放已配置的資源、關閉開啟的檔案等)。天真地說,我們會這樣實作

function* genFunc() {
    yield 'a';
    yield 'b';

    console.log('Performing cleanup');
}

在正常的 for-of 迴圈中,一切都很好

for (const x of genFunc()) {
    console.log(x);
}
// Output:
// a
// b
// Performing cleanup

但是,如果您在第一次 yield 後結束迴圈,執行看似永遠停在那裡,永遠不會到達清除步驟

for (const x of genFunc()) {
    console.log(x);
    break;
}
// Output:
// a

實際上發生的是,每當有人提早離開 for-of 迴圈時,for-of 就會將 return() 傳送至目前的迭代器。這表示不會到達清除步驟,因為產生器函式會在之前傳回。

感謝的是,這很容易透過在 finally 子句中執行清除來修正

function* genFunc() {
    try {
        yield 'a';
        yield 'b';
    } finally {
        console.log('Performing cleanup');
    }
}

現在一切都按照預期運作

for (const x of genFunc()) {
    console.log(x);
    break;
}
// Output:
// a
// Performing cleanup

因此,使用需要以某種方式關閉或清除的資源的通用模式如下

function* funcThatUsesResource() {
    const resource = allocateResource();
    try {
        ···
    } finally {
        resource.deallocate();
    }
}
21.8.2.4 處理手動實作的迭代器中的清除
const iterable = {
    [Symbol.iterator]() {
        function hasNextValue() { ··· }
        function getNextValue() { ··· }
        function cleanUp() { ··· }
        let returnedDoneResult = false;
        return {
            next() {
                if (hasNextValue()) {
                    const value = getNextValue();
                    return { done: false, value: value };
                } else {
                    if (!returnedDoneResult) {
                        // Client receives first `done` iterator result
                        // => won’t call `return()`
                        cleanUp();
                        returnedDoneResult = true;
                    }
                    return { done: true, value: undefined };
                }
            },
            return() {
                cleanUp();
            }
        };
    }
}

請注意,當你第一次要傳回 done 迭代器結果時,你必須呼叫 cleanUp()。你不能提早執行,因為這樣 return() 仍然可能會被呼叫。這可能會很棘手。

21.8.2.5 關閉你使用的迭代器

如果你使用迭代器,你應該適當地關閉它們。在產生器中,你可以讓 for-of 為你完成所有工作

/**
 * Converts a (potentially infinite) sequence of
 * iterated values into a sequence of length `n`
 */
function* take(n, iterable) {
    for (const x of iterable) {
        if (n <= 0) {
            break; // closes iterable
        }
        n--;
        yield x;
    }
}

如果你手動管理事物,則需要更多工作

function* take(n, iterable) {
    const iterator = iterable[Symbol.iterator]();
    while (true) {
        const {value, done} = iterator.next();
        if (done) break; // exhausted
        if (n <= 0) {
            // Abrupt exit
            maybeCloseIterator(iterator);
            break;
        }
        yield value;
        n--;
    }
}
function maybeCloseIterator(iterator) {
    if (typeof iterator.return === 'function') {
        iterator.return();
    }
}

如果你不使用產生器,則需要更多工作

function take(n, iterable) {
    const iter = iterable[Symbol.iterator]();
    return {
        [Symbol.iterator]() {
            return this;
        },
        next() {
            if (n > 0) {
                n--;
                return iter.next();
            } else {
                maybeCloseIterator(iter);
                return { done: true };
            }
        },
        return() {
            n = 0;
            maybeCloseIterator(iter);
        }
    };
}

21.8.3 檢查清單

下一頁:22. 產生器