突然ですが、Javascriptのイテレータとジェネレータの定義について説明できますか? 普段なんとなく使っている両者ですが、厳密な定義となるとなかなか調べる機会もないのではないでしょうか? そこで今回は、Javascriptにおけるイテレータとジェネレータについてご紹介します。
Javascript
の基本的な文法が分かるIterator
)についてイテレータ(Iterator
)について知る前にまず、イテレータリザルト(Iterator Result
)について知る必要があります。
Iterator Result
)の定義イテレータリザルトは、プロパティにvalue
とdone
を持つオブジェクトを指します。
例えば、下記のresult
もイテレータリザルトです。
const obj = {
value : 'hoge',
done : false
}
value
はany
でdone
はboolean
です。
Iterator
)の定義イテレータはnext()
関数をプロパティに持つオブジェクトです。
next()`はイテレータリザルトを返す必要があります。
例えば下記のiteratorObj
はイテレータです。
const iteratorObj = {
next : function () {
return {
value : 'fuga',
done : false
}
}
}
console.log(iteratorObj.next()); // --> { value : 'fuga', done : false }
Iterable
)の定義イテラブルであるとは、[Symbol.iterator]()
を実行することでイテレータを返すオブジェクトを指します。
ここでのSymbol.iterator
は予約語です。
例えば下記のiterableObj
はイテラブルです。
const iteratorObj = {
next : function () {
return {
value : 'fuga',
done : false
}
}
}
const iterableObj = {
[Symbol.iterator] : function () {
return iteratorObj;
}
}
イテラブルな型でメジャーなものは以下の通りです。
Array
はなんとなく知っているという方も多いですが、実はString
もイテラブルです。
Array
String
Iterator
(イテレータそのものがイテラブルである。[Symbol.iterator]()
を実行した場合は自身を返す)下記にイテラブルの扱い方のTips
を紹介します。
for(v of iterable)
最もメジャーなのがfor(v of iterable)
構文です。
内部的には下記の4ステップを行なっています。
iterable[Symbol.iterator]()
を実行しイテレータを生成next()
を実行しイテレータリザルトを取得done
がtrue
なら処理を終了done
がfalse
なら、value
をv
に代入して処理を実行例えば、下記のような書き方を何気なく使ったことがあると思いますが、これはイテレータを取り出して使用しています。
const array = ["A", "B", "C"];
// array[Symbol.iterator]()を実行しiteratorを取得
// iterator.next().done === trueなら処理を終了
for(const v of array) { // iterator.next().done === falseならvにvalueを代入し処理を行う
console.log(v);
}
// => A
// => B
// => C
...
と組み合わせスプレッド演算子...
と組み合わせることで出力される全てのイテレータリザルトのvalue
の配列が生成できます。
const array = [1, 10, 5];
console.log(Math.max(...array));
// => 10
また、下記のように分割代入することもできます。
const array = ['hoge', 'fuga', 'puni'];
const [ a, b, c ] = array;
console.log(a);
// => hoge
console.log(b);
// => fuga
console.log(c);
// => puni
Generator
)についてジェネレータ(Generator
)はイテレータの生成をサポートする関数およびオブジェクトです。
ジェネレータを使わずに独自でイテレータを作成することもできますが、下記のような問題が発生します。
value
やdone
の値を内部的に管理する必要があるdone
の生成にバグが含まれていた場合、常にdone=false
となりfor(v of iterator)
構文等で無限ループが発生するGenerator
)の定義ジェネレータ(Generator
)は後述のジェネレータ関数から生成されたオブジェクトです。
ジェネレータはイテラブルでかつイテレータです。
ジェネレータ関数は、ジェネレータを生成する関数です。
function*
で宣言し、内部でyield
およびyield*
を用いることができます。
ジェネレータ関数は実行時にジェネレータを返却し、返却されたジェネレータのnext()
を実行していくことで関数中のyield
を順に辿っていきます。
下記がジェネレータの最も基本的な形です。
// ジェネレータ関数
function* generatorFunc (n) {
yield n;
n++;
yield n;
n++;
yield n;
return n;
}
// ジェネレータを生成
var generator = generatorFunc(1);
// 最初のyieldの値がvalueに入る
console.log(generator.next()) // => { value : 1, done : false }
// 2つめのyieldに値がvalueに入る
console.log(generator.next()) // => { value : 2, done : false }
// 3つめのyieldに値がvalueに入る
console.log(generator.next()) // => { value : 3, done : false }
// 関数の実行が終了したのでdoneがtrueになった
console.log(generator.next()) // => { value : 3, done : true }
ここでのポイントは、先に述べたようにジェネレータ関数のよって生成されたジェネレータgenerator
のnext()
を実行するたびに、ジェネレータ関数内のyield
を辿っている点です。
ジェネレータ関数の末端に達するタイミングでdone
がtrue
で返却されます。
ジェネレータはイテラブルなイテレータなので、当然次のように記載することもできます。
function* generatorFunc (n) {
yield n;
n++;
yield n;
n++;
yield n;
return n;
}
var generator = generatorFunc(1);
for(const v of generator){
console.log(v);
}
// => 1
// => 2
// => 3
ジェネレータはreturn()
で途中終了することができます。
この場合、終了時のyield
以降の実行されません。
function* generatorFunc (n) {
yield n;
n++;
yield n;
n++;
yield n;
return n;
}
var generator = generatorFunc(1);
console.log(generator.next()) // => { value : 1, done : false }
console.log(generator.next()) // => { value : 2, done : false }
console.log(generator.return()) // => { value : undefiend, done : true }
// -> 2回目のn++は実行されず終了する
console.log(generator.next()) // => { value : undefiend, done : true }
yield*
)yield*
を用いることで、ジェネレータ中でイテレータを扱うことができます。
当然、ジェネレータもイテレータであるので、ジェネレータ中にジェネレータを扱うこともできます。
function* generatorFunc (n) {
// yield*でイテレータを渡せる
yield* [n,++n,++n];
return n;
}
var generator = generatorFunc(1);
// 以下結果は基本形の時と同じ
console.log(generator.next()) // => { value : 1, done : false }
console.log(generator.next()) // => { value : 2, done : false }
console.log(generator.next()) // => { value : 3, done : false }
console.log(generator.next()) // => { value : 3, done : true }
next()の引数に値を渡すことで、ジェネレータ関数の値を動的にセットすることができます。
値は、直前に実行されたyieldを受けた変数
に代入されます。
function* generatorFunc () {
const x = yield true ;
const y = yield true ;
yield { x, y };
return { x, y }
}
var generator = generatorFunc();
console.log(generator.next(0)) // => { value : true, done : false }
// 直前にnext()を実行した際のyieldを受けた変数xに10が入る
console.log(generator.next(10)) // => { value : true, done : false }
// 直前にnext()を実行した際のyieldを受けた変数yに20が入る
console.log(generator.next(20)) // => { value : { x : 10, y : 20 }, done : false }
console.log(generator.next()) // => { value : { x : 10, y : 20 }, done : true }
ここまでジェネレータの特徴として、「イテレータの生成が容易になる」という点にフォーカスしてきましたが、もう一つ大きな特徴があります。
それは、ジェネレータ関数を任意のタイミングで停止・再開できる関数
として扱うことができるという点です。
例えば、ジェネレータ関数中に非同期処理を挟み込むことで、処理を同期的に扱うことができるようになります。
※この特製を応用したものとして、redux-saga
等のライブラリが挙げられます。
今回は、Javascript
におけるイテレータIterator
とジェネレータGenerator
について定義と使用方法をご紹介しました。
普段何気なく使っているfor (v in iterator)
構文も実はそれらを応用したものだと分かります。
あまり自作のイテレータ・ジェネレータを使うことはないかもしれませんが、基礎知識として覚えておいて損はないと思います。