arguments
for-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
ループでは、早期(または仕様言語では突然の)終了は、次によって発生する可能性があります。
break
continue
(外部ループを続行する場合、continue
はbreak
のように動作します)throw
return
これらの場合、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-of
yield*
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
を介してイテレータをクローズすることを忘れないでください。これを正しく行うのは難しい場合があります。