argumentsfor-ofループArray.from()...)yield*return()とthrow()ES6はデータの走査のための新しいメカニズムであるイテレーションを導入しました。イテレーションの中心となる2つの概念があります。
Symbol.iteratorであるメソッドを実装することで実現されます。そのメソッドはイテレータのファクトリです。TypeScript表記でインターフェースとして表現すると、これらの役割は以下のようになります。
interface Iterable {
[Symbol.iterator]() : Iterator;
}
interface Iterator {
next() : IteratorResult;
}
interface IteratorResult {
value: any;
done: boolean;
}
以下の値はイテラブルです。
プレーンオブジェクトはイテラブルではありません(理由は専用のセクションで説明されています)。
イテレーションを介してデータにアクセスする言語構文
const [a,b] = new Set(['a', 'b', 'c']);
for-ofループ for (const x of ['a', 'b', 'c']) {
console.log(x);
}
Array.from():
const arr = Array.from(new Set(['a', 'b', 'c']));
...) const arr = [...new Set(['a', 'b', 'c'])];
const map = new Map([[false, 'no'], [true, 'yes']]);
const set = new Set(['a', 'b', 'c']);
Promise.all()、Promise.race() Promise.all(iterableOverPromises).then(···);
Promise.race(iterableOverPromises).then(···);
yield*:
yield* anIterable;
イテラビリティの概念は以下の通りです。
for-ofは値をループし、スプレッド演算子 (...) は値を配列または関数呼び出しに挿入します。すべてのコンシューマがすべてのソースをサポートするのは現実的ではありません。特に、新しいソース(例:ライブラリ経由)を作成できる必要があるためです。そのため、ES6はIterableインターフェースを導入しました。データコンシューマはそれを使い、データソースはそれを実装します。
JavaScriptにはインターフェースがないため、Iterableはむしろ規約です。
Symbol.iteratorであるメソッドを持ち、いわゆるイテレータを返す場合、イテラブルとみなされます。イテレータは、そのメソッドnext()を介して値を返すオブジェクトです。つまり、メソッド呼び出しごとに1つずつ、イテラブルのアイテム(コンテンツ)を反復処理します。配列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つの値を返します。つまり、イテラブルなデータ構造が非線形(ツリーなど)の場合、イテレーションはそれを線形化します。
さまざまな種類のイテラブルデータの反復処理には、for-ofループ(「for-ofループ」章を参照)を使用します。
配列(および型付き配列)は、その要素に対してイテラブルです。
for (const x of ['a', 'b']) {
console.log(x);
}
// Output:
// 'a'
// 'b'
文字列はイテラブルですが、Unicodeコードポイントを反復処理します。各コードポイントは、1つまたは2つのJavaScript文字で構成される場合があります。
for (const x of 'a\uD83D\uDC0A') {
console.log(x);
}
// Output:
// 'a'
// '\uD83D\uDC0A' (crocodile emoji)
マップは、そのエントリに対してイテラブルです。各エントリは[キー、値]ペアとしてエンコードされ、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はイテラブルではないことに注意してください。
セットはその要素に対してイテラブルです(セットに追加されたのと同じ順序で反復処理されます)。
const set = new Set().add('a').add('b');
for (const x of set) {
console.log(x);
}
// Output:
// 'a'
// 'b'
WeakSetはイテラブルではないことに注意してください。
arguments 特殊変数argumentsはECMAScript 6ではほとんど廃止されています(restパラメータのため)が、イテラブルです。
function printArgs() {
for (const x of arguments) {
console.log(x);
}
}
printArgs('a', 'b');
// Output:
// 'a'
// 'b'
ほとんどのDOMデータ構造は最終的にイテラブルになります。
for (const node of document.querySelectorAll('div')) {
···
}
この機能の実装は開発中であることに注意してください。しかし、シンボルSymbol.iteratorは既存のプロパティキーと競合しないため、比較的簡単に実装できます。
すべてのイテラブルコンテンツがデータ構造から来る必要はありません。オンザフライで計算することもできます。例えば、主要なES6データ構造(配列、型付き配列、マップ、セット)には、イテラブルオブジェクトを返す3つのメソッドがあります。
entries()は、[キー、値]配列としてエンコードされたエントリに対してイテラブルを返します。配列の場合、値は配列要素であり、キーはそのインデックスです。セットの場合、各キーと値は同じです(セット要素)。keys()は、エントリのキーに対するイテラブルを返します。values()は、エントリの値に対するイテラブルを返します。それがどのように見えるか見てみましょう。entries()は、配列要素とそのインデックスを取得する良い方法を提供します。
const arr = ['a', 'b', 'c'];
for (const pair of arr.entries()) {
console.log(pair);
}
// Output:
// [0, 'a']
// [1, 'b']
// [2, 'c']
プレーンオブジェクト(オブジェクトリテラルによって作成される)はイテラブルではありません。
for (const x of {}) { // TypeError
console.log(x);
}
オブジェクトがデフォルトでプロパティに対してイテラブルではないのはなぜですか?その理由は以下のとおりです。JavaScriptでは2つのレベルでイテレーションを行うことができます。
プロパティの反復処理をデフォルトにすることは、これらのレベルを混在させることを意味し、2つの欠点があります。
エンジンがメソッドObject.prototype[Symbol.iterator]()を介してイテラビリティを実装する場合、追加の注意点があります。Object.create(null)を介して作成されたオブジェクトはイテラブルになりません。これは、Object.prototypeがそのプロトタイプチェーンにないためです。
オブジェクトのプロパティを反復処理することは、オブジェクトをマップとして使用する場合にのみ興味深いことを覚えておくことが重要です1。しかし、これはES5では、より良い代替手段がないために行います。ECMAScript 6では、組み込みデータ構造Mapがあります。
プロパティを適切に(安全に)反復処理する方法は、ツール関数を使用することです。例えば、objectEntries()を使用します。その実装は後で示します(将来のECMAScriptバージョンには、同様のものが組み込まれている可能性があります)。
const obj = { first: 'Jane', last: 'Doe' };
for (const [key,value] of objectEntries(obj)) {
console.log(`${key}: ${value}`);
}
// Output:
// first: Jane
// last: Doe
次のES6言語構文は、イテレーションプロトコルを使用します。
for-ofループArray.from()...)Promise.all()、Promise.race()yield*次のセクションでは、それぞれを詳しく説明します。
配列パターンによるデストラクチャリングは、任意のイテラブルに対して機能します。
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'];
for-ofループ for-ofはECMAScript 6の新しいループです。基本的な形式は以下のようになります。
for (const x of iterable) {
···
}
詳細については、「for-ofループ」章を参照してください。
iterableのイテラビリティが必要であることに注意してください。そうでなければ、for-ofは値をループできません。つまり、イテラブルでない値は、イテラブルなものに変換する必要があります。例えば、Array.from()を使用します。
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()の詳細については、配列に関する章を参照してください。
...) スプレッド演算子は、イテラブルの値を配列に挿入します。
> const arr = ['b', 'c'];
> ['a', ...arr, 'd']
['a', 'b', 'c', 'd']
つまり、任意のイテラブルを配列に変換するコンパクトな方法を提供します。
const arr = [...iterable];
スプレッド構文は、反復可能オブジェクトを関数、メソッド、またはコンストラクタ呼び出しの引数に変換します。
> Math.max(...[-1, 8, 3])
8
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
WeakMapとWeakSetのコンストラクタも同様に機能します。さらに、MapとSetはそれ自体が反復可能オブジェクトです(WeakMapとWeakSetはそうではありません)。つまり、それらのコンストラクタを使用して複製できます。
Promise.all()とPromise.race()は、Promiseの反復可能オブジェクトを受け入れます。
Promise.all(iterableOverPromises).then(···);
Promise.race(iterableOverPromises).then(···);
yield* yield*は、ジェネレータ内でのみ使用可能な演算子です。これは、反復可能オブジェクトによって反復処理されるすべてのアイテムを生成します。
function* yieldAllValuesOf(iterable) {
yield* iterable;
}
yield*の最も重要なユースケースは、(反復可能なものを生成する)ジェネレータを再帰的に呼び出すことです。
このセクションでは、反復可能オブジェクトの実装方法を詳細に説明します。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'を返し、最後に反復処理の終了を示します。各アイテムは、プロパティを持つオブジェクトにラップされます。
valueは実際のアイテムを保持し、doneは、終了したかどうかを示すブール型のフラグです。doneがfalseの場合、valueがundefinedの場合、省略できます。つまり、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
反復可能オブジェクトとイテレータが同じオブジェクトの場合、前の関数を簡素化できます。
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で処理する前に、初期のアイテム(ヘッダーなど)を削除できることです。
return()とthrow() 2つのイテレータメソッドはオプションです。
return()により、反復処理が途中で終了した場合に、イテレータがクリーンアップする機会が与えられます。throw()は、yield*を介して反復処理されるジェネレータへのメソッド呼び出しを転送することについてです。ジェネレータに関する章で説明されています。return()によるイテレータのクローズ 前述のように、オプションのイテレータメソッドreturn()は、最後まで反復処理されなかった場合に、イテレータがクリーンアップできるようにするためのものです。これはイテレータをクローズします。for-ofループでは、早期(または仕様言語では突然の)終了は、次によって発生する可能性があります。
breakcontinue(外部ループを続行する場合、continueはbreakのように動作します)throwreturnこれらの場合、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文を処理する方法によるものであり、ジェネレータに関する章で説明します。
次の構成要素は、完全に「使い果たされていない」イテレータを閉じます。
for-ofyield*Array.from()Map()、Set()、WeakMap()、WeakSet()Promise.all()、Promise.race()後のセクションでは、イテレータのクローズについて詳しく説明します。
このセクションでは、反復可能オブジェクトのさらにいくつかの例を見ていきます。これらの反復可能オブジェクトのほとんどは、ジェネレータを使用してより簡単に実装できます。ジェネレータに関する章で方法を示します。
反復可能オブジェクトを返すツール関数とメソッドは、反復可能データ構造と同じくらい重要です。以下は、オブジェクトの独自の属性を反復処理するためのツール関数です。
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]] };
}
};
}
コンビネータ4は、既存の反復可能オブジェクトを組み合わせて新しい反復可能オブジェクトを作成する関数です。
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
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']
一部の反復可能オブジェクトは、決して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]
next()の呼び出しごとに新しいオブジェクトが作成されるため、反復プロトコルが遅いのではないかと心配しているかもしれません。しかし、小さなオブジェクトのメモリ管理は最新のエンジンでは高速であり、長期的には、エンジンは反復処理を最適化して、中間オブジェクトを割り当てる必要がなくなります。es-discussのスレッドに詳細情報があります。
原則として、イテレータが同じ反復結果オブジェクトを複数回再利用するのを妨げるものはありません。ほとんどのことはうまくいくと予想されます。ただし、クライアントが反復結果をキャッシュする場合は問題が発生します。
const iterationResults = [];
const iterator = iterable[Symbol.iterator]();
let iterationResult;
while (!(iterationResult = iterator.next()).done) {
iterationResults.push(iterationResult);
}
イテレータがその反復結果オブジェクトを再利用する場合、iterationResultsには一般的に同じオブジェクトが複数回含まれます。
ECMAScript 6に反復可能オブジェクトのコンビネータ、つまり反復可能オブジェクトを操作したり作成したりするためのツールがないのはなぜかと疑問に思われるかもしれません。それは、2段階で進める計画があるためです。
最終的に、そのようなライブラリまたはいくつかのライブラリの一部がJavaScript標準ライブラリに追加されます。
そのようなライブラリの外観を把握したい場合は、標準Pythonモジュールitertoolsをご覧ください。
はい、反復可能オブジェクトは手動で実装する場合は実装が難しいです。次の章では、このタスク(その他のこと)に役立つジェネレータを紹介します。
反復プロトコルは、次のインターフェースで構成されています(yield*によってのみサポートされ、オプションであるIteratorのthrow()は省略しました)。
interface Iterable {
[Symbol.iterator]() : Iterator;
}
interface Iterator {
next() : IteratorResult;
return?(value? : any) : IteratorResult;
}
interface IteratorResult {
value : any;
done : boolean;
}
next()のルール
xを持っている限り、next()はオブジェクト{ value: x, done: false }を返します。next()は常にプロパティdoneがtrueであるオブジェクトを返す必要があります。IteratorResult イテレータ結果のdoneプロパティは、trueまたはfalseである必要はありません。真偽値であれば十分です。すべての組み込み言語メカニズムでは、done: falseを省略できます。
いくつかのイテラブルは、要求されるたびに新しいイテレータを生成します。例えば、配列などです。
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を返し、常に同じイテレータ(それ自身)を返すことを意味します。
反復処理プロトコルでは、イテレータを終了する2つの方法が区別されます。
doneがtrueであるオブジェクトが返されるまでnext()を呼び出します。return()を呼び出すことで、もうnext()を呼び出さないことをイテレータに伝えます。return()の呼び出しに関する規則
return()はオプションのメソッドであり、すべてのイテレータが持っているわけではありません。それを持っているイテレータは、クローズ可能と呼ばれます。return()は、イテレータが枯渇していない場合にのみ呼び出す必要があります。例えば、for-ofは、それが「突然」(終了する前に)終了されたときはいつでもreturn()を呼び出します。次の操作は、突然の終了を引き起こします:break、continue(外部ブロックのラベル付き)、return、throw。return()の実装に関する規則
return(x)は通常、オブジェクト{ done: true, value: x }を生成する必要がありますが、言語メカニズムは、結果がオブジェクトでない場合にのみエラーをスローします(仕様のソース)。return()が呼び出された後、next()によって返されるオブジェクトもdoneである必要があります。次のコードは、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!
イテレータが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
次のクラスは、イテレータのクローズを防ぐための一般的なソリューションです。これは、イテレータをラップし、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
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();
}
}
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()がまだ呼び出される可能性があります。これは正しく行うのが難しい場合があります。
イテレータを使用する場合は、適切にクローズする必要があります。ジェネレータでは、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);
}
};
}
return()が呼び出された場合、クリーンアップアクティビティを実行する必要があります。try-finallyを使用して、単一の位置で両方に対処できます。return()によってイテレータがクローズされた後、next()を介してイテレータ結果を生成してはなりません。for-ofなどを使用する場合を除く)returnを介してイテレータをクローズすることを忘れないでください。これを正しく行うのは難しい場合があります。