JavaScriptのGeneratorがわからない
何をしているのか、どのような場面で利用すると得なのかがわからない。
何をしているのか
イテレータを簡単に実装できる。また、ジェネレータ内の処理は途中で一旦停止できる。
イテレータ
順番に結果を返却できるオブジェクト。配列や文字列など。for(v of iterable)
の形で利用できる。
const array = [1,2,3,4,5] //イテレータ
for (const v of array){
console.log(v)
}
ジェネレータの利用
1から5までの数字の配列を繰り返したいなあというときは、配列を作ってしまえば問題ない。ジェネレータは繰り返したい数がわかっていないときなどに利用できる。
//ジェネレータ関数
function* gen(from, to){
while(from<=to){
yield from++
}
}
//gがジェネレータ
const g = gen(0, 20) //0, 20といった数字は実行時に決められる。
for (const v of g){
console.log(v)
}
利用すると嬉しい場面
ただ、これだけだと以下のようにジェネレータを利用しなくても似たことはできる。
function gen(from, to) {
let array = [];
for (let i = from; i <= to; i++) {
array.push(i);
}
return array;
}
const g = gen(0, 20);
g.map((e) => console.log(e));
どうやら便利なのは、yield
で処理が一旦停止するというところらしい。
配列だとすべての要素を持たないと次の処理に移行できないが、ジェネレータだと一要素ずつ生成される。1万個の要素を持つ配列を初期化してから扱うか、1万個まで要素を返却する関数を作成して、実行時に各要素を初期化して扱うか(遅延評価)、という違いがある。メモリ効率は良くなる。
また、関心の分離もできる。わかりやすかったので「ジェネレーター関数と仲良くなろう!」から引用させてもらいます。
//1, 1, 2, 3, 5, 8, … と、フィボナッチ数列を出力する。ただし3の倍数と3を含む数字列の場合は前後に “!!!” を付加する。(1000くらいで適当に切り上げる。)
//ジェネレータを利用しない版
//処理を全部ループの中に含めている
let prev = 0
let current = 1
while (current < 1000) {
if (current % 3 === 0 || current.toString().includes('3')) {
console.log(`!!! ${current} !!!`)
} else {
console.log(`${current}`)
}
[prev, current] = [current, prev + current]
}
//ジェネレータを利用する版
function *fibonacci() {
let prev = 0
let current = 1
while (current < 1000) {
yield current; // 次の行が[]なので、このセミコロンは必須
[prev, current] = [current, prev + current]
}
}
for (const n of fibonacci()) {
if (n % 3 === 0 || n.toString().includes('3')) {
console.log(`!!! ${n} !!!`)
} else {
console.log(`${n}`)
}
}
ジェネレータを利用しなくても実装できるが、「関数の分割(関心の分離)」と「メモリ効率の追求(遅延評価)」を同時に行いたい場面ではジェネレータを利用する意味はある。処理の分離だけしたい場合は、フィボナッチ数列を全部生成した後で、フィルターすればよい。メモリ効率だけ追求したい場合は、生成と同時にフィルターすればよい。両方を同時に行いたい場合は、ジェネレータで「要素を生成するロジック」だけを切り出して、そこにフィルター処理を適用する。
あと具体的には、APIから取得したデータをフィルタリングしていく場合に利用できる。ここがまだわかっていないのでまた今度に回す。
参考
JavaScript の ジェネレータ を極める!
ジェネレーター関数と仲良くなろう!
JavaScriptのGeneratorを使うとなぜ関数型プログラミングが捗るのか調べてみた