ジェネレーター関数とは
ジェネレーターを理解するためには、先にジェネレーター関数について知っておくと理解が早いと思います。
ジェネレーター関数とは
ジェネレーター関数はイテレーターを作るための関数です。
これはfunction*
(最後に*
がつく)キーワードによって定義できます。
function* 関数名(引数) {
// 処理
}
イテレーターとは
イテレーターは何かを列挙するためのオブジェクトです。
next
メソッドを持ち、このメソッドが{ done: boolean, value: T }
形式のオブジェクトを返す場合、それはイテレーターと呼べます。
また、Symbol.itereator
メソッドを持ち、これがイテレーターを返す場合、そのオブジェクトはイテラブルだと呼べます。
イテラブルなオブジェクトはfor...of
やスプレッド構文が使えます。
詳細はMDNをご覧ください。
また、ジェネレーター関数は後述のジェネレーターを返します。
ジェネレーターはとりあえずイテレーターのようなものだと思ってください。
yield
ジェネレーター関数において、イテレーターのnext
メソッドが返す値はyield
を使うことで指定できます。
yield
は通常の関数で使うことはできません。
構文はyield 返したい値
です。
function* numbers() {
yield 1
yield 2
yield 3
}
例: 1~10を表現する
例えば、1~10を表現するイテレーターを作成したいと思います。
イテレーターは使い捨てにしたいので、簡単にイテレーターのオブジェクトを作成できる方法を用意する必要があります。
今回はmakeIterator
というジェネレーター関数を用意し、これを呼び出すと新しくイテレーター(ジェネレーター)が作成されるようにします。
普通に実装した場合
ジェネレーター関数を使わず普通に実装した場合は、おおよそ以下のようになると思います。
function makeIterator() {
// クロージャで状態を管理
let current = 0
// イテレーターを返す
return {
next() {
current++
// すでに10を超えている場合
if (current > 10) return { done: true }
return { done: false, value: current }
}
}
}
const iterator = makeIterator()
iterator.next() // { done: false, value: 1 }
iterator.next() // { done: false, value: 2 }
// ...
iterator.next() // { done: false, value: 10 }
iterator.next() // { done: true }
この方法だとネストが深くなり、少し冗長な印象があります。
ジェネレーター関数を使った場合
// function*キーワードでジェネレーター関数を定義
function* makeIterator() {
let current = 1
while (current <= 10) {
yield current
current++
}
}
const iterator = makeIterator()
iterator.next() // { done: false, value: 1 }
iterator.next() // { done: false, value: 2 }
// ...
iterator.next() // { done: false, value: 10 }
iterator.next() // { done: true, value: undefined }
ジェネレーター関数では、普通のwhile
の中にyield
を入れることで、簡潔に実装できます。
ネストも減ってコードも読みやすくなったと思います。
ジェネレーターとは
ジェネレーターは、ジェネレーター関数を呼び出すと得られるオブジェクトです。
ジェネレーターは以下の性質を持っています。
-
イテレーターである:
next
メソッドを持ち、その戻り値がイテレータープロトコルに従っているため - イテラブルである:
[Symbol.iterator]
メソッドがthis
=イテレーターを返すため(反復可能プロトコルに従っているため)
上で定義したmakeIterator
関数はジェネレーターを返します。
そしてジェネレーターはイテレーターなので、next
メソッドを呼び出すことができました。
イテラブルである
ジェネレーターはイテラブルなので、for...of
文やスプレッド構文が直接使えます。
const iterator = makeIterator() // ジェネレーターを作成
const array = [...iterator] // スプレッド構文で配列に変換
// [1, 2, 3, ..., 9, 10]
もしジェネレーター関数を使わずに同じことをやろうとすると、少し大変です。
イテレーターは通常イテラブルではないので、[Symbol.iterator]
メソッドを持ったオブジェクトでラップする必要があります。
function makeItereator() {
// ジェネレーター関数を使わずに実装する
}
function makeIterable() {
return {
[Symbol.iterator]() {
return makeIterator()
}
}
}
const iterable = makeIterable()
const array = [...iterable]
// [1, 2, 3, ..., 9, 10]
もしくは[Symbol.iterator]
メソッドに実装を書くことになるでしょう。
作例: range
ジェネレーター関数なら、範囲を示すrange
の簡単な実装もできます。
ここでのrange
は以下の引数を取ります。
-
start
: 範囲の開始地点 -
end
: 範囲の終了地点、end
自身は含まない -
step
: 範囲内の数値をいくつづつ増やすか、デフォルトでは1
実装は先ほどとほぼ同じようにできます。
ここでは以下のようになりました。
function* createRange(start: number, end: number, step: number = 1) {
let current = start // currentを初期化
// currentがendを超えるまでyield
while (current < end) {
yield current
current += step // stepだけcurrentを増やす
}
}
const range = createRange(1, 11); // 1 ~ 10(11は含まない)
[...range] // [1, 2, 3, ..., 10, 11]
このrange
にはジェネレーターが入っています。
ただのジェネレーターなので、スプレッド構文に入れたり、直接for...of
ループに入れたりできます。