【JavaScript】 Generator(ジェネレーター)とyieldってなんだ?関数とは何が違うのか
更新日:2020/02/29
JavaScriptの機能にGenerator(ジェネレーター)というものがあります。
途中で処理を止めて、止めたところから再実行できる不思議ちゃんな関数です。
今回はジェネレーター関数の作り方と、使いどころについてお伝えします。
ジェネレーター関数の例
まずはジェネレーター関数のイメージをつかむために、簡単な例を紹介します。
HTML
<p id="Generator">click</p>
<div id="resArea"></div>
JavaScript
function* GeneratorTest( startPer ) { // ジェネレーター関数
let cper = startPer;
while(1) { // 無限ループ!!
const per = Math.floor( Math.random() * 100 );
const nextParam = yield ( per <= cper ) ? 1 : 0;
cper = ( nextParam === 0) ? cper + 10 : startPer;
}
}
window.addEventListener( "DOMContentLoaded", () => {
const a = GeneratorTest(10);
let res = null;
const resArea = document.getElementById( "resArea" );
document.getElementById( "Generator")
.addEventListener( "click", ()=>{
res = ( res !== null ) ? a.next( res.value ) : a.next( );
resArea.innerHTML += ( ( res.value === 1)
? "<span style=\"color:red\">あたり!</span> "
: "はずれ ");
});
});
上のコードのGeneratorTestという名前の関数が、ジェネレーター関数です。
関数内では、ランダムで得たパーセントが引数で与えれたパーセントより小さければ”あたり”として1を返しています。
再度呼び出された場合は、比較するパーセントに10を足して、当たる確率を上げています。
どんな挙動をするか、次の「click」をクリックして確かめてみてください。
click
GeneratorTest内のコードは無限ループしています。
それなのに関数からの返り値を取得しています。
さらに再呼び出しでは、関数内の変数の値が保たれている。
これは中断して、再開したと考えると納得できますね。
ジェネレーター関数の作り方
まずはサクッと、ジェネレーター関数の作り方を解説します。
function*で宣言する
ジェネレーター関数は、通常の関数と同じような宣言をします。
一点異なるのが、 function の後に * (アスタリスク)をつける点です。
ジェネレーター関数の宣言
function* generator([引数, 引数......]) {
// 処理の文
}
一時停止したい位置でyield
一時停止したい位置に、yield文を記述します。
いくつ書いても大丈夫です。
ジェネレーター関数の宣言
function* generator([引数, 引数......]) {
// 処理の文
yield 戻り値;
// 処理の文
yield 戻り値;
// 処理の文
yield 戻り値;
}
yieldは次のように書くことができます。
yield* イテラブルなオブジェクト
Arrayなどの反復呼び出し可能なオブジェクトをイテラブルなオブジェクトといいます。
参考:【JavaScript】 Iterator(イテレーター)とは?避けて通りたいけど説明してみる
yieldの後にアスタリスクを書き、戻り値としてイテラブルなオブジェクトを指定すると、順番に値を取り出すことができます。
次の文は、
yield* [1,2,3,4,5];
このように連続してyieldが記述されているイメージです。
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
再開時の引数を得る
再開時の引数を、yield文の結果として取得します。
変数に代入したり、そのまま比較しましょう。
ジェネレーター関数の宣言
function* generator([引数, 引数......]) {
// 処理の文
let res = yield 戻り値;
// 処理の文
res = yield 戻り値;
// 処理の文
if( (yield 戻り値) === 1) {}
}
ジェネレーター関数の使い方
次に宣言したジェネレーター関数の使い方です。
ジェネレーターオブジェクトを作る
宣言したジェネレーター関数を式として記述すると、ジェネレーターオブジェクトが作成されます。
次の例は、ジェネレーター関数を実行しているのではないので注意しましょう。
let a = GeneratorTest(10); // ジェネレーターオブジェクトの作成(初期化)
また同じコードから複数回作成したジェネレーターオブジェクトは、それぞれ別のものです。
内部の変数などは共通のものではありません。
let a = GeneratorTest( 10 ); // ジェネレーターオブジェクトの作成(初期化)
let b = GeneratorTest( 50 ); // 別のジェネレーターオブジェクトの作成(初期化)
ジェネレーターオブジェクトから値を得る
作成したジェネレーターオブジェクトはnext()メソッド持っています。
このメソッドを使用することで、yieldまで実行して値を得ることができます。
let res = a.next( );
next()で得た値について
実はジェネレーターはイテレータとしての機能を持つオブジェクトです。
イテレータ?
という人は、次のページを見てください。
参考:【JavaScript】 Iterator(イテレーター)とは?避けて通りたいけど説明してみる
イテレータは、next()の実行結果として次のものを返します。
■値があるとき
{ value: 値, done: false }
■値がないとき
{ value: 戻り値, done: true }
次の例のように、valueとdoneの値を参照して、処理を進めることができます。
function* GeneratorTest( ) {
yield *[ 1 , 2 ];
return "end";
}
let b = GeneratorTest();
while( true ){
let c= b.next();
console.log( c ); // 結果: Object { value: 1, done: false }
// Object { value: 2, done: false }
// Object { value: "end", done: true }
if( c.done) break;
}
ちなみに、GeneratorTest()の最後の return は、なくてもかまいません。
return文がない、またはreturn文に値がない場合、次のオブジェクトが返ります。
Object { value: undefined, done: true }
再開時にジェネレーターオブジェクトに値を与える
next()メソッドに引数を指定することで、再開時にジェネレーターオブジェクトに値を与えることができます。
function* GeneratorTest( ) {
let res = yield 1;
console.log( res ); // 100
}
let a = GeneratorTest();
console.log( a.next() ); // Object { value: 1, done: false }
a.next( 100 );
上の例では、最初のa.next()でyieldまで実行し一時的に中断します。
2回目のa.next( 100 )で、変数res に100が代入されます。
ジェネレーター関数から見た場合、yieldの実行結果と考えるとしっくりとくるかもしれませんね。
ちなみにnext()メソッドに引数を指定していない場合、undefinedがセットされます。
ジェネレーターの処理を強制的に終わらせる
ジェネレーターの値を残っている状態で、それ以上値を取得させたくないことがあります。
一つの案として、次のようにコードを記述します。
function* GeneratorTest( ) {
let res = yield 1;
if( res === false ) return;
res = yield 2;
if( res === false ) return;
}
next()の引数にfalseが指定されたら、returnで処理を終了しています。
この方法だと、全てのyieldで判定文を書かないといけなくなり、かなり面倒ですね。
そこで次のように、throw()を使って例外を発生させると楽です。
function* GeneratorTest( ) {
try {
yield 1;
yield 2;
} catch (e) {
console.log(e.message); // 中断!
}
}
let a = GeneratorTest();
console.log(a.next()); // Object { value: 1, done: false }
console.log(a.throw(new Error("中断!"))); // Object { value: undefined, done: true }
console.log(a.next()); // Object { value: undefined, done: true }
例外捕捉後にyield文がなければ、それ以上値を取得できません。
catchが、処理を終わらせているのではないので注意。
逆に例外が発生しても値を返し続けたい場合は、catchで捕捉したあともyield文に処理を回せばOKです。
function* GeneratorTest( ) {
while(true){
try {
yield 1;
} catch (e) {
console.log(e.message); // 中断!
}
}
}
let a = GeneratorTest();
console.log( a.next() ); // Object { value: 1, done: false }
console.log(a.throw(new Error("中断!"))); // Object { value: 1, done: false }
console.log( a.next() ); // Object { value: 1, done: false }
console.log( a.next() ); // Object { value: 1, done: false }
ジェネレーター関数をイテラブルなオブジェクトとして扱う
ジェネレーターはイテレーターとしての機能を持つと書きましたが、イテラブルなオブジェクトでもあります。
イテラブルなオブジェクトとは、イテレーターを作成して返す[Symbol.iterator]()というメソッドを持つオブジェクトです。
ちょと難しいです
とりあえず、こちらを見てみてください(しつこく3回目)
参考:【JavaScript】 Iterator(イテレーター)とは?避けて通りたいけど説明してみる
ジェネレーターをfor...of 文で使用する
ジェネレーターはイテラブルなオブジェクトなので、for...of 文を使用できます。
function* GeneratorTest( v ) {
yield* [ 1 , 2 , 3 ].map( ( n ) => n ** v );
}
let a = GeneratorTest( 3 );
for( let val of a){
console.log(val); // 結果: 1
// 8
// 27
}
スプレッド構文でArrayに変換
JavaScriptにはスプレッド構文というものがあって、イテラブルなオブジェクトを簡単にArrayに変換できます。
スプレッド構文でArrayに変換
function* GeneratorTest( v ) {
yield* [ 1 , 2 , 3 ].map( ( n ) => n ** v );
}
let a = [...GeneratorTest( 3 )];
console.log(a); // Array(3) [ 1, 8, 27 ]
スプレッド構文については、次のページを参考にしてみてください!
参考:【JavaScript】 コード中の「...」は意味があった
ただし、while(true)なので無限に値を作り続けるジェネレーターにスプレッド構文を使用すると、ひどい目にあいます。
一度やってみてください!!
一度やってみるべきコード
function* GeneratorTest( ) {
let i = 0;
while(1){
yield i;
i++;
}
}
let a = [...GeneratorTest( )];
console.log(a);
ジェネレーターをイテラブルなオブジェクトに組み込む
【JavaScript】 Iterator(イテレーター)とは?避けて通りたいけど説明してみるで、イテラブルなオブジェクトも作り方を解説しています。
しかしジェネレーターを使用すると、もっと簡単にイテラブルなオブジェクトを作成できるケースがあります。
次の例は、イテレーターを使ったイテラブルなオブジェクトの例です。
イテラブルなオブジェクト
const a = {
[Symbol.iterator] : function(){ // [Symbol.iterator]() { と記述可能
return {
count:0,
next: function() {
this.count ++;
return ( this.count <= 3 ) ?
{ value: this.count * 2 , done:false }
: { done: true };
}
}
}
};
for( let val of a){
console.log(val); // 結果: 2
// 4
// 6
}
ジェネレーターに書き換えてみます。
ジェネレーターを使ったイテラブルなオブジェクト
const a = {
[Symbol.iterator] : function* (){ // *[Symbol.iterator]() { と記述可能
let count = 0;
while(count < 3){
count ++;
yield count * 2;
}
}
};
for( let val of a){
console.log(val); // 結果: 2
// 4
// 6
}
イテレーターとしての返り値を意識しなくて済むのと、内部の変数の取り扱いを気にしなくていいのでとても楽ですね。
ジェネレーターの使いどころ
ジェネレーターの利点は、それ単体でイテラブルなオブジェクトとして使用できるところです。
なんらかの法則に従って値を連続して求めるなど力を発揮してくれます。
ジェネレーターは、yieldで処理が停止するという点の着目した利用法も考えられます。
例えば処理ステップ数が異なるコードで、状況見ながら処理を進めるなどが考えられます。
function* a1(){
// 何らかの処理1
yield flg;
// 何らかの処理2
yield flg;
// 何らかの処理3
return flg;
}
function* a2(){
// 何らかの処理1
yield flg;
// 何らかの処理2
return flg;
}
let g = (判定) ? a1() : a2();
while(1){
let flg = g.next();
if( flg.value ) { // 処理状況の判断 }
if( flg.done ) break;
}
うまく使えば便利ですよね。
ただコードの視認性の問題ですが、yield文は頻繁に行方不明になります。
あまり目立たないやつなので、コードが複雑になると後で非常に苦労したりします。
できるかぎり簡潔になるように気を配るべきですね。
ジェネレーターを使ってみた
ジェネレーターの効果的な使用方法を考えてみた。
しかし思いつきませんでした。
そこで勇者に魔王を倒してもらうことにしました。
(意味不明)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script>
window.addEventListener('DOMContentLoaded',()=> {
const param = document.getElementById("hp");
const comamnd = document.getElementById("comamnd");
const resArea = document.getElementById("res");
const paramSet = hp => param.innerText=hp.toString();
paramSet(10);
const resAreaAdd = (msg) => {
resArea.innerHTML += "<p>" + msg + "</p>";
resArea.scrollTop = resArea.scrollHeight;
};
// 選択コマンド
const command1 = "<p>どうする? クリックせよ!</p><p id=\"hosii\">>くれ!</p><p id=\"kougeki\">>うるさい!</p>";
const command2 = "<p>どうする? クリックせよ!</p><p id=\"kougeki\">>攻撃!</p>";
function* attack(hp){ // ジェネレータ
let count = 0;
while(1){
let clickId = yield (hp <= 5) ? command1 : command2;
if( clickId === "hosii") {
resAreaAdd("・・・・世界は滅亡した");
return "Bad End";
}else{
paramSet(--hp);
resAreaAdd( (++ count) + "回目:" + "魔王は1ポイントのダメージを受けた!");
if( hp < 1 ) {
resAreaAdd("魔王は滅びた!");
return "Haapy End";
}
if( hp <= 5 ) resAreaAdd("イベント発生!!魔王が世界の半分をお前にやるといっている!!");
}
}
}
let ce = attack(10); // ジェネレータ初期化
const clickEvent = (id) => { // クリックイベント
if( id==="start" || id === "hosii" || id === "kougeki" ) {
comamnd.innerHTML = ce.next( id ).value; // コマンド選択画面セット
}
};
resAreaAdd("散歩してたら魔王がいきなりでてきた!!");
clickEvent("start"); // 初期画面を作成
comamnd.addEventListener("click", e=> clickEvent(e.target.id) );
});
</script>
<style>
#param,#res,#comamnd{
border:1px solid #333;
}
#res{
height: 200px;
overflow:auto;
}
#comamnd p{
cursor: pointer;
}
</style>
</head>
<body>
<div id="param">魔王:残りHP <span id="hp"></span></div>
<div id="res"></div>
<div id="comamnd"></div>
</body>
</html>
適当すぎて、鼻血が出そう・・・
まとめ
ジェネレーターを効果的に使用すれば、簡潔なコードを書けそうですね。
ただしかっこいいからという理由で使いそうな、やばい予感が…
ジェネレーターを使用したために難解なコードになるのは本末転倒ですね。
使いどころをよく考えるべきだと思います。
追記:
この記事は僕がまだ let と const の使い分けができていない頃に作成しています。
そのため、同じ状況でも let と constが入り乱れています。
ご了承ください。
使い分けについては、次のページを見てね!
■【JavaScript】 適当に使ってた!変数宣言var/let/constの使い分け
更新日:2020/02/29
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。