21. イテラブルとイテレータ
目次
本書をサポートしてください:購入 (PDF、EPUB、MOBI) または 寄付
(広告、ブロックしないでください。)

21. イテラブルとイテレータ



21.1 概要

ES6はデータの走査のための新しいメカニズムであるイテレーションを導入しました。イテレーションの中心となる2つの概念があります。

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とイテレータは、いわゆるプロトコルインターフェースとそれらを使用するためのルール)の一部です。このプロトコルの重要な特性は、シーケンシャルであることです。イテレータは一度に1つの値を返します。つまり、イテラブルなデータ構造が非線形(ツリーなど)の場合、イテレーションはそれを線形化します。

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コードポイントを反復処理します。各コードポイントは、1つまたは2つのJavaScript文字で構成される場合があります。

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

21.3.3 マップ

マップは、そのエントリに対してイテラブルです。各エントリは[キー、値]ペアとしてエンコードされ、2つの要素を持つ配列です。エントリは常に決定的に、マップに追加されたのと同じ順序で反復処理されます。

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

WeakMapはイテラブルではないことに注意してください。

21.3.4 セット

セットはその要素に対してイテラブルです(セットに追加されたのと同じ順序で反復処理されます)。

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

WeakSetはイテラブルではないことに注意してください。

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データ構造(配列、型付き配列、マップ、セット)には、イテラブルオブジェクトを返す3つのメソッドがあります。

それがどのように見えるか見てみましょう。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では2つのレベルでイテレーションを行うことができます。

  1. プログラムレベル:プロパティを反復処理することは、プログラムの構造を調べることを意味します。
  2. データレベル:データ構造を反復処理することは、プログラムによって管理されているデータを調べることを意味します。

プロパティの反復処理をデフォルトにすることは、これらのレベルを混在させることを意味し、2つの欠点があります。

エンジンがメソッドObject.prototype[Symbol.iterator]()を介してイテラビリティを実装する場合、追加の注意点があります。Object.create(null)を介して作成されたオブジェクトはイテラブルになりません。これは、Object.prototypeがそのプロトタイプチェーンにないためです。

オブジェクトのプロパティを反復処理することは、オブジェクトをマップとして使用する場合にのみ興味深いことを覚えておくことが重要です1。しかし、これは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のコンストラクタは、[キー、値]ペアの反復可能オブジェクトを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

このコードは3つのステップを実行し、カウンタstepによってすべてが正しい順序で実行されます。まず、値'hello'を返し、次に値'world'を返し、最後に反復処理の終了を示します。各アイテムは、プロパティを持つオブジェクトにラップされます。

donefalseの場合、valueundefinedの場合、省略できます。つまり、switch文は次のように記述できます。

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

ジェネレータに関する章で説明されているように、done: trueの最後のアイテムにもvalueを持たせたい場合があります。そうでなければ、next()はよりシンプルになり、アイテムを直接返すことができます(オブジェクトにラップせずに)。反復処理の終了は、特別な値(たとえば、シンボル)で示されます。

反復可能オブジェクトのもう1つの実装を見てみましょう。関数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
}

反復処理を続ける1つのユースケースは、実際のコンテンツをfor-ofで処理する前に、初期のアイテム(ヘッダーなど)を削除できることです。

21.5.2 オプションのイテレータメソッド:return()throw()

2つのイテレータメソッドはオプションです。

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)

iterableの先頭n個のアイテムの反復可能オブジェクトを返すコンビネータ関数take(n, iterable)から始めましょう。

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)

zipは、n個の反復可能オブジェクトを、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()は1つの可能性です。

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に反復可能オブジェクトのコンビネータ、つまり反復可能オブジェクトを操作したり作成したりするためのツールがないのはなぜかと疑問に思われるかもしれません。それは、2段階で進める計画があるためです。

最終的に、そのようなライブラリまたはいくつかのライブラリの一部がJavaScript標準ライブラリに追加されます。

そのようなライブラリの外観を把握したい場合は、標準Pythonモジュールitertoolsをご覧ください。

21.7.4 反復可能オブジェクトは実装が難しいですか?

はい、反復可能オブジェクトは手動で実装する場合は実装が難しいです。次の章では、このタスク(その他のこと)に役立つジェネレータを紹介します。

21.8 ECMAScript 6反復プロトコルの詳細

反復プロトコルは、次のインターフェースで構成されています(yield*によってのみサポートされ、オプションであるIteratorthrow()は省略しました)。

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プロパティは、trueまたはfalseである必要はありません。真偽値であれば十分です。すべての組み込み言語メカニズムでは、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 イテレータのクローズ

反復処理プロトコルでは、イテレータを終了する2つの方法が区別されます。

return()の呼び出しに関する規則

return()の実装に関する規則

次のコードは、for-ofループが、doneイテレータ結果を受け取る前に中断された場合に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()内の2番目のループには反復処理するものがありません。

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

ジェネレータをクローズ不能にするもう1つの方法があります。ジェネレータ関数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. ジェネレータ