8. JS魔法修行: (6) - "材料" を寄せ集めて作る
作成中
今回は今までとは違うアプローチを試してみましょう。
これまでは完成されたプログラムを先に読み、解説をあとから読むことでプログラムを学んできました。今回は "事前に材料を物色する" ということをやってみます。
あまりにも技術者たちが暇なとき、ブログを読んだり、プロジェクトに関連する技術のドキュメントを読んだり、ほかの技術者たちとギークな会話をしたりします。そして新しいことを学ぶと、「へー、今度やってみよう」 と、みっともない笑みを浮かべます。そういう "材料" がいつも技術者の頭にあります。他の妄想とともに。
一つひとつの "材料" を読んでもらうと分かりますが、意外に長いです。でも読んでみると、なかなか面白いと思います。プログラミングに限らず、自分の能力の可能性を拡げてくれる知識というのは人間にとっての本質的な喜びですよね。技術者には知的好奇心に溢れた人が多く、彼らが暇なときにこういった記事を読むのは、それらを読むことで自分のプログラミングの幅が拡がるからです。
という訳でこの章は皆さんのスキルアップを目的とし、時間のあるときに読み進めていただくものです。みなさんの知的好奇心が刺激されるはずですから、あまり硬くならないで、技術的な読み物として楽しんでみて下さい。
完成したらどんな感じ
"材料" を読み進め、スキルを身につけたら、最後にそれらを用いて簡単なアニメーションを作ります。
完成するとそれがどんな感じになるか見てみましょう。
なのでこのページで動作するサンプルを用意してみました。以下のサンプルでは Start ボタンを押すと、player と enemy が動き始めます。二つが衝突すると初期位置に戻ります。途中で Stop ボタンを押したときも初期位置に戻ります。
さっそく Start ボタンを押してみましょう。
ちなみに 「開発ツール」 を開くと以下のようなログ出力がみられます。
イメージが掴めたところで、一つずつ "材料" をみていきましょう。
8-1. 材料: 論理式
高校のとき、AND と OR をやったのを覚えていますか? これがなければプログラミングは成り立ちません。論理式 と呼ばれるそれを JS の場合は 「ブラウザが評価する」 のですがそこで重要になるのは、論理式を構成する AND と OR の "順番" です。この "順番" を上手く使うと長ったらしいプログラムが短くなったりします。しかし JS の特性上、実はそれが論理を評価するということ以上の意味を持ってくることがあります。そのことを学んでいきます。
OR
まずは OR からいきましょう。日本語だと 「または〜」 ですね。JS だと || と書きます。
例を見てみましょう。
例を見てみましょう。
const guest = {
joe: 'ugly',
pete: 'hard working'
};
if (guest.steve || guest.dave) {
console.log('I see some friends');
} else {
console.log('None of them are friends');
}
joe と pete は友だちです。だけどやって来たのは steve と dave です。guest.steve || guest.dave という論理式で判定します。しかし steve も dave も guest に含まれてません。なのでこれは None of them are friends と出力されます。
直感的にはこの結果が else の方に流れていくのは分かります。しかし判定結果として if の中には何が入っているのでしょうか? ここではしれっと流しましたが、これは次の例で判明します。
では論理式の別の使い方を見てみましょう。
こちらは論理式の判定の結果を name という変数に入れるという例です。
こちらは論理式の判定の結果を name という変数に入れるという例です。
const guest = {
joe: 'ugly',
pete: 'hard working'
};
const name = guest.steve || guest.dave;
ブラウザはまず guest.steve || guest.dave という論理式のうち、一番目の方である guest.steve を評価します。つまり guest.steve が存在するかどうか確認します。すると guest オブジェクトに steve なんて存在しないことが判明します。この時点でこの式の評価は失敗し、二番目である guest.dave の方をブラウザは "評価しようとさえ" しません。そして一番目の guest.steve の値が name の中に入ります。といっても guest.steve などというのは "全く存在しない" ので、値として入るのは undefined というものです。
"全く存在しない" という表現を聞いて、以前やった null が気になると思います。こっちは 「何も入っていない」 でした。これと undefined はどう違うのか? ただの言葉遊びじゃないか、なめてんのか、てめえ金返せ! と、真っ赤になって激怒することでしょう。実は undefined は "本当に何も入っていない" ときに使います。厳密にいえばむしろ "定義すらされていない" ことを表します。こう書いたら分かりやすいかも知れません。
const banana = 3;
const apple = null;
console.log(typeof banana); // --> number
console.log(typeof apple); // --> object
console.log(typeof orange); // --> undefined
ちなみに上記で typeof というのを使っていますが、これはその変数の "型" を教えてくれるものです。これを見ると null の型が object になっているのが判ります。つまり apple には実はその中に 「オブジェクト」 が入っているのです。何のオブジェクトかというと 「何も入っていない」 を表す null オブジェクトです。
一方で orange のほうは何も入っていないというより、そもそも 「定義すらされていない」 状態にあります。なので型としても undefined となっているのです。
というわけで、さっき "直感" で else に流れると分かった先ほどの例で、if の中には undefined が入っていたのでした。
つまりはこういうことです。
if (undefined) {
console.log('I see some friends');
} else {
console.log('None of them are friends');
}
AND
AND の方もやりましょう。日本語だと 「〜かつ〜」 ですね。
const askForDate = ({ salary, appearance }) => {
if (salary > 10000000 && appearance > 80) {
console.log('You can ask me for a date');
}
}
askForDate({ salary: 12000000, appearance: 90 });
// --> 付き合ってあげてもいいわよ
年収 1,000 万円以上で、見ためが 80 点以上のときだけ、彼女をデートに誘えます。
さて、この場合 if の中には何が入っているのでしょう?
まずブラウザは論理式の最初の式を評価しようとします。salary > 10000000 です。評価の結果は true です。つまり boolean (論理型) です。一つ目が true だったとしても二つ目は違うかも知れません。AND で評価しているので二つ目も調べる必要があります。appearance > 80 の結果は true です。こちらも boolean (論理型) が入っていました。
まずブラウザは論理式の最初の式を評価しようとします。salary > 10000000 です。評価の結果は true です。つまり boolean (論理型) です。一つ目が true だったとしても二つ目は違うかも知れません。AND で評価しているので二つ目も調べる必要があります。appearance > 80 の結果は true です。こちらも boolean (論理型) が入っていました。
つまりはこういうことです。
if (true && true) {
...
}
したがって結果として if の中に入っていたのは true という boolean (論理型) でした。
論理演算子
論理式が使えるようになると、今度は 「論理演算子」 が使えるようになります。英語では Conditional Operator と呼ばれます。このような形で使われます。
条件 ? 真であるとき : 偽であるとき
さっきの askForDate の中に条件式がありました。あれを使ってみましょう。
const salary = 12000000;
const appearance = 90;
let result = 'no';
if (salary > 10000000 && appearance > 80) {
result = 'yes';
}
console.log(result); // --> yes
「論理演算子」 を使うと、これはこのように書けます。
const salary = 12000000;
const appearance = 90;
const result = (salary > 10000000 && appearance > 80) ? 'yes' : 'no';
console.log(result); // --> yes
何がどんなふうにすごいかって?
まず if と else の条件分岐が 「一行になった」 じゃないですか。それだけでスゴいでしょう。
でも本当にすごいのは let で宣言していた result を const で宣言できるようになった ことです。
えっ?そっちのすごさのほうが分からない?
考えてみて下さい。朝はいつもパンだとします。そうするとパン屋さんに行って買ったものは 「いつもパンであって欲しい」 のです。でもある朝、袋を開けてみたら "サバ" が入っていました。ぼくは温厚なので店員に怒ったりはしません。でも なるべくなら袋には "パン" が入っていて欲しい。 そんな願いは贅沢でしょうか?
パン屋さんもプロなんだから、お客様にサバを渡しちゃいけません。そんなハプニングが起きないよう、パン屋さんはできるかぎりの努力をするべきです。
これは 「関数」 もおなじです。
関数ではさまざまな変数をあつかいます。関数を利用する側からすれば、関数からいつもおなじものが返ってきて欲しい。その期待に応えられるように努力できる点があるとすれば、関数の中で できるだけ変化を発生させない ことです。const を率先して使って let の使用を最小限にとどめるのは、そういった配慮のためです。全てはお客様のためにあるからです。
このような思想を 「関数型プログラミング (Functional Programming)」 といいます。そもそも "関数" は "函数" と書きます。つまり 「函 (ハコ = ブラックボックス)」 のことです。ブラックボックスの中で何がおこなわれているかは知らずとも、ハコに何かを放り込んだら、いつも期待した通りのものが返ってきて欲しい。それを徹底するのが 「関数型プログラミング」 のミッションです。
ちょっと熱く語ってしまいました。
すみません。
本題に戻ります。
すみません。
本題に戻ります。
さて askForDate は console.log するだけの関数でした。なので論理演算子をやるために上で使った条件式を、boolean (論理型) を返すような関数にしてみましょう。
const askForDate = ({ salary, appearance }) => {
return (salary > 10000000 && appearance > 80) ? true : false;
}
const result = askForDate({ salary: 1000, appearance: 10 });
const message = result ?
'You can ask me for a date' :
'Look at yourself in the mirror';
console.log(message); // --> あんた鏡をみなさいよ
というか以下の部分はすでに bool 型です。
(salary > 10000000 && appearance > 80)
なのでこう書けますね。
const askForDate = ({ salary, appearance }) => {
return (salary > 10000000 && appearance > 80);
}
これはもう "論理演算子" の例じゃなくなっていますが、、、
関数の中身が一行しかないので、短くしたいならこうも書けますね。
const askForDate = ({ salary, appearance }) => salary > 10000000 && appearance > 80;
なんと一行になってしまいました!
"論理式" ってすごいですね。
"論理式" ってすごいですね。
8-2. 材料: 複数行コメントと JSDoc
コメントは有用です。他人のコードを読むということがこれからどんどん増えますが、親切なコメントに出会ったとき、コメントを書いた開発者に心から感謝したくなるときがあります。コメントを書くということは、それを読む人への配慮です。時にはそれだけでなく、コメントがあったことによって九死に一生を得るほどの意味を持つこともあります。これはあとでも書きますが、大概それは数カ月後、完全に記憶から消え去った自分自身のソースコードを訪れるときです。ここでは特に JSDoc と呼ばれる作法を学びます。
複数行コメント
const girl = { name: 'Sarah', age: 4, spy: true }; // 女の子の情報
// パーティー会場に入れるかどうか判定する関数です。
const enterParty (age) => {
let result = false;
if (age > 18) { // 18才以上であればパーティーに参加できる
result = true;
}
return result;
};
if (enterParty(girl.age)) {
console.log(`${girl.name} may enter the party because she is over 18`);
} else {
console.log(`${girl.name} may NOT enter the party...`);
}
しかし // でコメントアウトできるのは "一行だけ" です。
複数行のコメントアウトは /* */ を使います。
複数行のコメントアウトは /* */ を使います。
/*
パーティー会場に入れるかどうか判定する関数です。
18才以上であればパーティーに参加できます。
入れないのは辛いですね。
*/
const enterParty (age) => {
...
...
};
ただこれは慣習として普通はこう書きます。
/**
* パーティー会場に入れるかどうか判定する関数です。
* 18才以上であればパーティーに参加できます。
* 入れないのは辛いですね。
*/
const enterParty (age) => {
...
...
};
読みやすいからです。
JSDoc
関数の説明のためにコメントを書くとき、その関数に 「渡される引数」 と、その 「返却値」 を記述することがよくあります。その記述の仕方にはルールがあって、他のプログラミング言語でもそうなのですが、JS にも JSDoc という仕様があります。基本的に引数は @param を羅列していきます。返却値は普通は一個だけですので @returns が一つだけ記されます。大雑把なルールは以下の通りです。
/**
* 関数の説明
* @param {型1} [変数名1] - 説明1
* @param {型2} [変数名2] - 説明2
* @returns {返却される型}
*/
function foo (引数1, 引数2) {
...
...
};
なので enterParty 関数ならこうなります。
/**
* パーティー会場に入れるかどうか判定する関数です。
* 18才以上であればパーティーに参加できます。
* 入れないのは辛いですね。
* @param {number} [age] - 調べたい人の年齢
* @returns {boolean}
*/
const enterParty = (age) => {
...
...
};
なんでこんなものが必要なのでしょうか? プログラムを書くほうじゃなくて、読む側の気持ちを考えてみて下さい。プログラムを書いている側は、自分で作った関数だから、その関数が何を受け取って、何を返すかを知っています。でも 読む人にしてみればそれらの関数を初めて見るわけですから、関数の中身を読まなくてもパッとソースコードをみて、何をどのように使えばいいのかが解ったほうが嬉しい わけです。だからこういうのを書くのです。
それに何年もプログラムを書いていると解るのですが、自分の書いたプログラムを数カ月後に読みなおすことになったときに普通は自分が何を書いたのかきれいさっぱり忘れていることが多い ものです。だからこれは "未来の自分のため" に書いていることにもなります。どれだけこれに助けられることか、経験から次第に理解するようになります。コメントは、特に関数の入出力の説明は、出来るかぎり書くようにしましょう。
例はできるだけ多い方がいいですね。(記法は違いますが) 他のプログラミング言語でも使われますからね。なのでもう一つ関数を取り上げてみましょう。さっきの askForDate の説明をしてみます。
/**
* 女の子をデートに誘ったとき、応じてくれるかどうかを判定する関数です。
* @param {Object} {o} - 情報のかたまり
* @param {Object} {o.salary} - 年収 (in JPY)
* @param {Object} {o.appearance} - 見ため (in 点)
* @returns {boolean}
*/
const askForDate = ({ salary, appearance }) => salary > 10000000 && appearance > 80;
この関数の場合は "引数が一つだけ" です。そしてそれは "オブジェクト" であり、デートのお誘いを判定するのに必要な二つの情報が中に入っています。なので、とりあえずは @param {Object} {o} - 情報のかたまり というふうに便宜上それを o と名づけています。それで "オブジェクト" の中に入っている実際の情報たちを o.salary やら o.appearance やらと表記して説明しています。実はこれは JSDoc の正しい表記ではありません。でもこのような表記の仕方は見やすいため、広く普及しています。
関数を "利用する側" の気持ちになってみて下さい。ソースコードを読まなくても、どんなふうに使えばいいのか何となく解りますね。こういうのこそが日本人の "おもてなし" の精神でしょう。
8-3. 材料: 引数の初期値
以下は消費税を計算してくれる関数です。
/**
* 税金を計算する関数です。
* @param {number} [n] - 税率を加算する対象
* @param {number} [tax] - 税率
* @returns {number}
*/
const addTax = (n, tax) => n + (n * tax);
const total = addTax(100, 0.1);
console.log(total); // --> 110
でももし tax を渡さないと
const total = addTax(100); // 第二引数に tax を渡さない
console.log(total); // --> NaN
NaN という結果が返されます。NaN というのは "計算できなかった" を意味します。tax が undefined なので 100 * undefined というのはどうやっても計算できないのです。
関数の引数には 初期値 を設定できます。
/**
* 税金を計算する関数です。
* @param {number} [n] - 税率を加算する対象
* @param {number} [tax=1.1] - 税率
* @returns {number}
*/
const addTax = (n, tax = 0.1) => n + (n * tax);
const total = addTax(100);
console.log(total); // --> 110
今度は計算できました。
JSDoc 形式によるコメントの @param も [tax=1.1] に変更したのが分かりますか? 初期値がある場合はこのように表記します。
8-4. 材料: destructure
destructure を覚えているでしょうか?
(参照: 「7-2. 隔離された空間をつくる - オブジェクトから中身を取り出す」)
(参照: 「7-2. 隔離された空間をつくる - オブジェクトから中身を取り出す」)
const sarah = {
name: 'Sarah',
age: 4,
spy: true
},
// sarah の中から name を取り出す
const { name } = sarah;
console.log(name); // --> "Sarah"
つまりこれは
// sarah の中から name を取り出す
const name = sarah.name;
というふうにするのと同じだということでした。
階層が一つ深くなっても同じです。
const girl = {
cathy: { name: 'Cathy', age: 18, spy: false },
anna: { name: 'Anna', age: 33, spy: false },
sarah: { name: 'Sarah', age: 4, spy: true },
lucy: { name: 'Lucy', age: 82, spy: false }
};
// girls の中から sarah を取り出す
const { sarah } = girls;
console.log(sarah);
// {
// name: 'Sarah',
// age: 4,
// spy: true
// }
では次いきます。
二つ下にある要素を取り出したいとき、通常はこう書きますね。
// girls の中から sarah.name を取り出す
const name = girls.sarah.name;
console.log(name); // --> "Sarah"
これは実はこう書けます。
// girls の中から sarah.name を取り出す
const { sarah: { name } } = girls;
console.log(name); // --> "Sarah"
ものすごく深くしてみましょう。
const place = {
shibuya: {
taishoken: {
name: 'Taisho-ken',
type: 'chinese',
address: '1-1-1 Dogenzaka, Shibuya, Tokyo',
tel: '03-1111-2222',
menu: {
ramen: { name: 'Ramen', price: 240 },
gyoza: { name: 'Gyoza', price: 100 }
}
}
}
};
// place の中から shibuya.taishoken.menu を取り出す.
// const menu = place.shibuya.taishoken.menu と同じ.
const { shibuya: { taishoken: { menu } } } = place;
const lunch = `${menu.ramen.name} (${menu.ramen.price} yen)`;
const lunch2 = `${menu.gyoza.name} (${menu.gyoza.price} yen)`;
console.log(lunch); // --> Ramen (240 yen)
console.log(lunch2); // --> Gyoza (100 yen)
ではこれを関数に渡すことにしてみましょうか。
/**
* 商品のラベルを作ってくれる関数です。
* @param {Object} [o] - 商品
* @param {Object} [o.name="gohan"] - 商品名
* @param {Object} [o.price=10] - 値段
* @returns {string}
*/
const getLabel = (item = { name: 'Gohan', price: 10 }) => {
return `${item.name} (${item.price} yen)`;
};
const { shibuya: { taishoken: { menu } } } = place;
const lunch = getLabel(menu.ramen);
const lunch2 = getLabel(menu.cheeseburger);
console.log(lunch); // --> Ramen (240 yen)
console.log(lunch2); // --> Gohan (10 yen)
どっちかっていうともはや destructure じゃなくて "引数の初期値" の例になっていますが・・・
渡される引数は一つです。名前のないオブジェクト。商品なので関数の中では item という名前を付けています。しかし 引数が渡されてこない とき、つまり中華料理屋なのに menu.cheeseburger などという無茶なオーダーをする輩に対して、店のオヤジはただの白飯を鼻っつらにバシッと叩きつけるようにしています。なので item には { name: 'Gohan', price: 10 } という初期値を設定しています。
8-5. 材料: reduce
女性が繊細なのに対し、男性は大雑把すぎるきらいがありますが、ときには面倒くさい手順を省き、一気に仕事を終わらせたいときがあります。一分一秒をも惜しむとき、どうせ腹の中で一緒になるのだからと 味噌汁をご飯にぶっかけて食事を済ませたい のが男の性でしょう。ようするに面倒臭いのでドカンと一発で仕事を終わらせたいのです。そういうときに reduce がどれだけ便利であるか、ここではそれを体感してもらいます。
forEach のおさらい、それと "インデックス"
const girls = ['Cathy', 'Anna', 'Sarah', 'Lucy'];
girls.forEach((name) => {
console.log(`${name} is lovely.`);
});
// Cathy is lovely.
// Anna is lovely.
// Sarah is lovely.
// Lucy is lovely.
forEach には
(name) => {
console.log(`${name} is lovely.`);
}
といった "関数" を指定しているので、それに名前をつけて変数とし、それを渡してもよかったのですね。
const girls = ['Cathy', 'Anna', 'Sarah', 'Lucy'];
const printGirl = (name) => {
console.log(`${name} is lovely.`);
}
girls.forEach(printGirl);
forEach に指定した printGirl が実行されると、printGirl には引数が渡されてくるとお伝えしました。配列の "要素" です。この例では文字列です。ここではそれに name という名前をつけて使っています。
forEach は実は 「二つ目の引数」 も渡してくれます。配列の "インデックス" です。
これはやりましたね。何でしたっけ? インデックスというのは 「配列の何番目の要素なのか」 をあらわす数字でした。
(参照: 「6-1. 七つの「型」のこと - (4) 配列型」)
(参照: 「6-1. 七つの「型」のこと - (4) 配列型」)
ここではそれを i と呼んで使ってみましょう。
const girls = ['Cathy', 'Anna', 'Sarah', 'Lucy'];
const printGirl = (name, i) => {
console.log(`No.${i + 1} - ${name} is lovely.`);
}
girls.forEach(printGirl);
// No.1 - Cathy is lovely.
// No.2 - Anna is lovely.
// No.3 - Sarah is lovely.
// No.4 - Lucy is lovely.
forEach を使って新しくオブジェクトを作る
girls 配列をぐるぐる回して dinner というオブジェクトを新しく作りたいとします。
forEach を使うと、次のようになります。
forEach を使うと、次のようになります。
const girls = [
{ key: 'cathy', name: 'Cathy', age: 18 },
{ key: 'anna', name: 'Anna', age: 33 },
{ key: 'sarah', name: 'Sarah', age: 4 },
{ key: 'lucy', name: 'Lucy', age: 82 }
];
const dinner = {}; // 空っぽのオブジェクトを作る
girls.forEach((chick, i) => {
const { key, name, age } = chick;
if (age >= 18 && age < 40) {
// 条件に合致していたら key をキーにして
// dinner オブジェクトに文字列を入れる
dinner[key] = `Taking ${name} (${age}) for a dinner!`;
}
});
// dinner オブジェクトの中身を確認してみる
console.log(dinner);
// {
// cathy: "Taking Cathy (18) out for a dinner!",
// anna: "Taking Anna (33) out for a dinner!"
// }
これは解りますでしょうか?
外側で宣言した const data = {} というオブジェクトがあって、forEach の内側の関数で data[key] = ... を使って data オブジェクトの中にどんどん入れていくわけです。const で宣言されていても、オブジェクトそのものではなく、オブジェクトの "要素" を変更するのであればdata オブジェクトは変更できるのでした。
(参照: 「6-1. 七つの「型」のこと - (5) オブジェクト型」)
(参照: 「6-1. 七つの「型」のこと - (5) オブジェクト型」)
ちなみに chick というのは英語で 「可愛い子」 という意味です。
reduce を使ってオブジェクトを作る
配列には forEach だけでなく、他にも便利な関数がいくつか用意されています。
reduce は配列に用意されたそれらのうちの一つです。
reduce は配列に用意されたそれらのうちの一つです。
同じことを reduce を使ってやってみましょう。
const girls = [
{ key: 'cathy', name: 'Cathy', age: 18 },
{ key: 'anna', name: 'Anna', age: 33 },
{ key: 'sarah', name: 'Sarah', age: 4 },
{ key: 'lucy', name: 'Lucy', age: 82 }
];
const dinner = girls.reduce((acc, chick, i) => {
const { key, name, age } = chick;
if (age >= 18 && age < 40) {
acc[key] = `Taking ${name} (${age}) for a dinner!`;
}
return acc;
}, {});
console.log(dinner);
分かりにくいので reduce の箇所を簡略化して見てみます。
girls.reduce((acc, chick, i) => {
acc[key] = `Taking ${name} (${age}) for a dinner!`;
return acc;
}, {});
reduce は二つの引数を受け取ります。
一つ目には 関数 を渡します。
二つ目には 初期値 を渡します。
二つ目には 初期値 を渡します。
こんな感じですね。
配列.reduce(関数, 初期値);
一つ目に渡している "関数" ってどれでしょう?
これのことです。
これのことです。
(acc, chick, i) => {
acc[key] = `Taking ${name} (${age}) for a dinner!`;
return acc;
}
"関数" ですね。
指定されているこの関数のことを reducer (リデューサー) といいます。
二つ目に渡している "初期値" というのはこれです。
{}
空っぽの {} というオブジェクトに reduce はどんどん "ブツ" を突っ込んでいきます。これは読み進めていただければ、何のことを言っているのか、少しずつ分かってきます。
次に reduce は、指定した "関数" に対し、"お礼に三つの引数" をくれます。
それぞれ acc, chick, i とここでは名づけていますが、変数名は自由です。
それぞれ acc, chick, i とここでは名づけていますが、変数名は自由です。
(acc, chick, i) => { ... }
一つ目の acc は 累積値 です。
二つ目の chick は 配列の要素 です。
三つ目の i は インデックス です。
二つ目の chick は 配列の要素 です。
三つ目の i は インデックス です。
二つ目と三つ目は forEach のときと同じなので、理解しやすいでしょう。一つ目の "累積値" とは何のことでしょうか。reduce に指定した関数の内部で acc[key] = "..." によって4回突っ込み、さらに return acc によって4回返されたブツの "累積" のことです。(4回というのは girls 配列の要素を4人分ぐるぐる回すからです)
変数名としてここで名づけた acc は "accumulated" (累積された) の略です。
まず、"ぐるぐる" の一回目で acc は空っぽの {} オブジェクトとしてやって来ます。これは "初期値" として私たちがそう指定したからです。
{}
一回目のときは Cathy を入れます。一つ目の要素は Cathy の情報ですからね。
次に "ぐるぐる" の二回目で acc の中には
{
cathy: "Taking Cathy (18) out for a dinner!"
}
というオブジェクトが入ってやって来ます。この回では Anna を入れます。
"ぐるぐる" の三回目になると、acc には Cathy と Anna の二人分が入ってやって来ます。
{
cathy: "Taking Cathy (18) out for a dinner!",
anna: "Taking Anna (33) out for a dinner!"
}
しかしこの回では if 分岐の条件が合致しません。なので acc には誰も入れません。
四回目も Cathy と Anna の二人分が入ってやって来ますが、やはりこの回も if 条件が合致しないので誰も入れません。
最終的に acc の中には Cathy と Anna の二人分の女の子の情報が入っています。それが reduce から返されます。つまり dinner にはそれが入ります。
確認のため、もう一度、reduce のコードを見てみましょう。
const girls = [
{ key: 'cathy', name: 'Cathy', age: 18 },
{ key: 'anna', name: 'Anna', age: 33 },
{ key: 'sarah', name: 'Sarah', age: 4 },
{ key: 'lucy', name: 'Lucy', age: 82 }
];
const dinner = girls.reduce((acc, chick, i) => {
const { key, name, age } = chick;
if (age >= 18 && age < 40) {
acc[key] = `Taking ${name} (${age}) for a dinner!`;
}
return acc;
}, {});
console.log(dinner);
reduce で 「数値」 を作ってみる
もうちょっと簡単な reduce の使い方をみてみましょうか。
const numbers = [10, 20, 30, 40];
const total = numbers.reduce((acc, num) => acc + num, 0);
console.log(total); // --> 100
初期値は 0 です。
一回目: acc = 0 がやって来ます。この回では 0 + 10 = 10 を入れます。
二回目: acc = 10 がやって来ます。この回では 10 + 20 = 30 を入れます。
三回目: acc = 30 がやって来ます。この回では 30 + 30 = 60 を入れます。
四回目: acc = 60 がやって来ます。この回では 60 + 40 = 100 を入れます。
最終的に total には 100 が入ります。
一回目: acc = 0 がやって来ます。この回では 0 + 10 = 10 を入れます。
二回目: acc = 10 がやって来ます。この回では 10 + 20 = 30 を入れます。
三回目: acc = 30 がやって来ます。この回では 30 + 30 = 60 を入れます。
四回目: acc = 60 がやって来ます。この回では 60 + 40 = 100 を入れます。
最終的に total には 100 が入ります。
reduce で 「配列」 を作ってみる
または配列をつくるケースもあるはずです。
const numbers = [10, 20, 30, 40];
const days = numbers.reduce((acc, num) => {
acc.push(`${num} days`);
return acc;
}, []);
console.log(days); // --> ["10 days", "20 days", "30 days, "40 days"]
初期値は [] です。
一回目: acc = [] がやって来ます。この回では "10 days" を入れます。
二回目: acc = ["10 days"] がやって来ます。この回では "20 days" を入れます。
三回目: acc = ["10 days", "20 days"] がやって来ます。この回では "30 days" を入れます。
四回目: acc = ["10 days", "20 days", "30 days"] がやって来ます。この回では "40 days" を入れます。
最終的に days には ["10 days", "20 days", "30 days", "40 days"] が入ります。
一回目: acc = [] がやって来ます。この回では "10 days" を入れます。
二回目: acc = ["10 days"] がやって来ます。この回では "20 days" を入れます。
三回目: acc = ["10 days", "20 days"] がやって来ます。この回では "30 days" を入れます。
四回目: acc = ["10 days", "20 days", "30 days"] がやって来ます。この回では "40 days" を入れます。
最終的に days には ["10 days", "20 days", "30 days", "40 days"] が入ります。
配列の場合は push よりも concat を使うことが多いようです。
これで一行減ります。
これで一行減ります。
const days = numbers.reduce((acc, num) => {
return acc.concat(`${num} days`);
}, []);
中身が一行しかないとき、こういう書き方もできるのでしたね。
これで最終的にたった一行で済ませられます。
これで最終的にたった一行で済ませられます。
const days = numbers.reduce((acc, num) => acc.concat(`${num} days`), []);
8-6. 材料: イベント・リスナー
イベント・リスナーは、プログラムで何らかのイベントが発生するのを常に "聞いておく" ものだと説明しました。これは 「5-3. モンスターを動かす: 解説編 - (a) DOM とその読み込みのこと」 でも書きました。だからこそそれがゲームに必須であることも 「5-1. モンスターを動かす」 でちょっと触れました。
実際ゲームで発生するイベントは "クリック" だけじゃないわけです。キーボードで Enter を押したときだったり、敵にぶつかったときだったり。ゲームに限らなければ、例えば、誰かから着信を受け取ったときや、携帯電話を左右に振ったとき、この世界が多様である分だけ、プログラムにはありとあらゆる "イベント" があり、その分だけ、それらを "聞いておく" ことが必要になります。
それらを "聞いておく" ための方法は、JS の場合、実はこれから説明する一つの方法しかありません。
さて、今までのやり方だと、例えば HTML がこうなっていた場合
onclick を仕掛けたことで start が実行されるため、JS の方では単純にこうすればよかったのでした。
function start () { console.log('Game has started!'); }
これには、もっと普遍的なやり方があります。
まずは HTML 要素に id をつけます。
それで JS では、まずはその HTML 要素を取得します。
で、それに 「イベント・リスナー」 を追加するのです。
で、それに 「イベント・リスナー」 を追加するのです。
window.onload = main;
let start;
function main () {
// HTML 要素 (DOM) を取得する
start = document.querySelector('#btn-start');
if (start) {
// それに "イベント・リスナー" をくっつける
start.addEventListener('click', start);
}
}
function start () { console.log('Game has started!'); }
この部分ですね。
// それに "イベント・リスナー" をくっつける
start.addEventListener('click', start);
何のイベントなのでしょう? 書いてありますね。click イベントです。この click イベントが発生したとき、start が呼ばれるようにしています。
形はこれだけです。どんなイベントもこれだけ。
Bluetooth スピーカーが近くに見つかったときや、動画をずっと録画しているときや、それを止めるときなど、JS に用意されているものであれば、この方法で関数を登録します。
「JS に用意されているもの」 のことを Web API といいます。
興味があれば Mozilla が仕様を公開しているので、
JS でいったい何が出来るのかを確認してみて下さい。
眺めているだけでワクワクするはずです。
Web APIs | MDN
https://developer.mozilla.org/en-US/docs/Web/API
https://developer.mozilla.org/en-US/docs/Web/API
8-7. 材料: requestAnimationFrame
イベントループを回すため、これまで setInterval と clearInterval を使っていました。でもそれらに似てはいるけれど、アニメーションに特化した requestAnimationFrame と cancelAnimationFrame というものが用意されています。これらを使うことができます。
setInterval で回すイベントループはあまり正確ではありません。なぜならパソコンによってブラウザにアニメーションを描画するときの処理速度が違うからです。パソコンが別の仕事をして忙しいとき、処理が追いつずにアニメーションが "カクカク" してしまうことがあるのです。
一方で requestAnimationFrame はパソコンの忙しさを考慮してくれます。"カクカク" して処理が遅れてしまうとき、requestAnimationFrame は、まるで中年男性の取り戻すべく失われた時間を慰めてくれるかのように、 登録される関数を正しい描画タイミングで実行してくれる。
setInterval の方の使い方をおさらいします。
let timerId = setInterval(update);
let x = 0;
let y = 0;
function update () {
x += 1;
y += 1;
if (x > 100 && timerId) {
clearInterval(timerId);
timerId = null;
}
}
こちらが requestAnimationFrame の使い方です。
let animId = requestAnimationFrame(update);
let x = 0;
let y = 0;
function update () {
x += 1;
y += 1;
if (x > 100 && animId) {
cancelAnimationFrame(animId);
animId = null;
}
// 仕組みが setInterval と違い、requestAnimationFrame のほうは
// 処理が終わるごとに、一回一回、次の処理を自分で再登録しなきゃいけない
requestAnimationFrame(update);
}
コメントにも書いていますが requestAnimationFrame の登録の仕方は setInterval とは "発想" が異なります。setInterval の方は、関数を一回登録すれば、あとは何もしなくても永遠にそれを繰り返してくれました。一方で requestAnimationFrame で udpate 関数を登録すると、関数を実行してくれるのは一回だけです。次に update 関数を実行するかどうかはプログラマに任されます。だから永遠にループを繰り返したい場合は、update 関数の中でもう一度 requestAnimationFrame を使って自分自身 (update 関数) を再登録する必要がああります。
また requestAnimationFrame は全てのブラウザに "共通" ではありません。ブラウザによっては関数名が違ったりします。なので通常はこうします。
// ブラウザによっては requestAnimationFrame が無いことがあるので、
// "もし無かった場合は" の条件分岐をつなげることで
// そのブラウザ用の requestAnimationFrame が使えるようにします。
const requestAnimationFrame =
window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame;
let animId = requestAnimationFrame(update);
let x = 0;
let y = 0;
...
...
ここで || が使われているのが分かりますか? さっきやった "論理式" です。
この論理式で最初の window.requestAnimationFrame が存在しているのであれば、そのままそれが変数に入ります。なければ window.mozRequestAnimationFrame を評価します。なければ次にいきます。その調子で最終的に何もない場合、一番最後の window.msRequestAnimationFrame が返されます。中身は undefined です。なので requestAnimationFrame 変数の中には undefined が入ります。
cancelAnimationFrame の方もおなじ事情でそのブラウザに合わせます。
// こちらも canselAnimationFrame がブラウザによっては無かったり
// するので、確実に使えるようにします。
const cancelAnimationFrame =
window.cancelAnimationFrame ||
window.mozCancelAnimation;
8-8. 材料: Error
Error オブジェクト
const err = new Error('You are doomed.');
8-9. 材料: モジュール
「ファクトリー・モデル」 を用いたモジュールの作り方を覚えていますか?
(参照: 「7-1. スコープで他人に迷惑を掛けない - よくあるスコープの例」)
(参照: 「7-1. スコープで他人に迷惑を掛けない - よくあるスコープの例」)
const player = createPlayer();
player.x = 100;
player.y = 100;
player.die();
console.log(`where? (${player.x}, ${player.y})`);
// --> where? (-50, -50)
/**
* player オブジェクトを作ってくれるファクトリー関数です。
* @returns {Object} - "player" object
*/
function createPlayer () {
const x = 0;
const y = 0;
const oo = { x, y, alive: true };
oo.reset = () => {
oo.x = -50;
oo.y = -50;
};
oo.die = () => {
oo.alive = false;
oo.reset();
};
return oo;
}
8-x. 材料 xxx: xxx
xxxx
8-x. 完成形
完成形のソースコードは以下のとおり。
// ブラウザによっては requestAnimationFrame が無いことがあるので、
// "もし無かった場合は" の条件分岐をつなげることで
// そのブラウザ用の requestAnimationFrame が使えるようにします。
const requestAnimationFrame =
window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame;
// こちらも canselAnimationFrame がブラウザによっては無かったり
// するので、確実に使えるようにします。
const cancelAnimationFrame =
window.cancelAnimationFrame ||
window.mozCancelAnimation;
const int = Math.trunc; // 浮動小数点を整数にする関数です。
// ゲームに必要となる HTML 要素 (DOM) たちの "キー" の配列
const LIST_OF_ELEMENT_KEYS =
['canvas', 'btn-start', 'btn-stop', 'player', 'enemy'];
let elems = [];
let canvas;
let btn;
let player;
let enemy;
let timeStart = null; // アニメーションが開始した時間 (特に使っていない)
let elapsed = 0; // アニメーションの経過時間 (特に使っていない)
let animId = null; // 登録されたアニメーションの ID
let inProgress = false; // アニメーションが動いているかどうかフラグ
window.onload = main;
/**
* window オブジェクトが読み込まれたら実行される関数です。
* @returns {void}
*/
function main () {
try {
elems = getElements();
btn = createButtons({ start: elems['btn-start'], stop: elems['btn-stop'] });
canvas = createCanvas(elems['canvas']);
player = createPlayer(elems['player'], { canvas, step: 2 });
enemy = createEnemy(elems['enemy'], { canvas, step: 2 });
player.draw();
enemy.draw();
} catch (e) {
console.warn(e);
}
}
/**
* ゲームに必要となる HTML 要素 (DOM) たちを一気にまとめて取得する関数です。
* @returns {Array} - A list of all the elements (required for the game).
*/
function getElements () {
return LIST_OF_ELEMENT_KEYS.reduce((acc, key) => {
acc[key] = document.querySelector(`#${key}`);
return acc;
}, {});
}
/**
* 現在のブラウザの横幅と高さを取得する関数です。
* @returns {Object} - { width, height }
*/
function screenSize () {
return { width: window.innerWidth, height: window.innerHeight };
}
/**
* ゲームが展開される領域 (canvas) のサイズを設定する関数です。
* @param {Object} [el] - "#canvas" element
* @returns {void}
*/
function createCanvas (el) {
if (!el) throw new Error('No "canvas" element');
const screen = screenSize();
let width = int(screen.width * 0.9);
let height = width;
const max = screen.height * 0.7;
// When "height" exceeds the browser height,
// set both "width" and "height" smaller.
if (height > max) {
console.log(`(height: ${height}) is bigger than (max: ${int(max)})`);
height = width = max;
}
el.style.width = `${width}px`;
el.style.height = `${height}px`;
return { el, width, height };
}
/**
* 「開始ボタン (start)」 と 「停止ボタン (stop)」 に "イベントリスナー" を仕掛ける関数です。
* @param {Object} [o]
* @param {Object} [o.start] - "#btn-start" element
* @param {Object} [o.stop] - "#btn-stop" element
* @returns {void}
*/
function createButtons ({ start, stop }) {
if (!start) throw new Error('No "start" element');
if (!stop) throw new Error('No "stop" element');
start.addEventListener('click', startAnime);
stop.addEventListener('click', stopAnime);
}
/**
* アニメーションを開始する (登録する) 関数です。
* @param {Object} [event] - click event
* @returns {void}
*/
function startAnime (event) {
console.log('++++ startAnime()');
if (!inProgress) {
inProgress = true;
elapsed = 0;
timeStart = new Date();
animId = requestAnimationFrame(update);
} else {
console.warn('Another animation in progress...');
}
}
/**
* 登録されているアニメーションを停止する関数です。
* @returns {void}
*/
function stopAnime (event) {
console.log('++++ stopAnime()');
if (inProgress && animId) {
cancelAnimationFrame(animId);
resetGame();
} else {
console.warn('No animation to stop');
}
}
/**
* ゲームを初期状態にする関数です。
* @returns {void}
*/
function resetGame () {
inProgress = false;
animId = null;
// cancelAnimationFrame でアニメーショを停止するとマイクロ秒の遅れがあるため、
// player と enemy の位置のリセットの仕事は setTimeout を使ってちょっとだけ
// あとに実行されるようにします。ここでは 1/100 秒にしています。
//
// Even when "player" and "enemy" are reset here as to reset the game,
// the very last registered animation frame still runs due to
// the nature of "cancelAnimationFrame" which takes several microseconds
// to actually stop the animation frames.
// So, here we need "setTimeout" to slightly delay the game reset.
setTimeout(() => {
player.reset();
player.draw();
enemy.reset();
enemy.draw();
}, 100);
}
/**
* ぐるぐる回るイベントループで毎回実行される関数です。
* 基本的には player と enemy の位置を更新します。
* 経過時間も記録していますが、これは特に使っていません。
* @returns {void}
*/
function update () {
if (!inProgress) return;
player.update();
player.draw();
enemy.update();
enemy.draw();
animId = requestAnimationFrame(update);
elapsed = new Date() - timeStart;
}
/**
* player オブジェクトを作ってくれるファクトリー関数です。
* @param {Object} [el] - "#player" element
* @param {Object} [options={}] - "#btn-stop" element
* @param {Object} [options.canvas] - "canvas" object
* @param {number} [options.step=1] - step(s) per animation frame
* @returns {Object} - "player" object
*/
function createPlayer (el, options = {}) {
if (!el) {
throw new Error('No "player"');
}
const { canvas, step = 1 } = options;
// player の本体となるオブジェクトです。
const $_ = {
el,
x: 0,
y: 0,
alive: true
};
const reset = () => {
$_.x = 10;
$_.y = 10;
console.log(`(player) (${$_.x}, ${$_.y})`);
};
// ゲームの方の update 関数の中で player.update() みたいに呼ばれます。
const update = () => {
if (!inProgress) return;
let x = $_.x;
let y = $_.y;
x += 1;
y += 1;
// ゲーム領域の外に出てしまったらアニメーションを停止します。
if (x > canvas.width || y > canvas.height) {
if (x > canvas.width) x = canvas.width;
if (y > canvas.height) y = canvas.height;
stopAnime();
}
// player が enemy を通り越し, enemy より右下に来たときも停止します。
if (x > enemy.x || y > enemy.y) {
stopAnime();
}
$_.x = x;
$_.y = y;
};
// END OF: update()
// ゲームの方の update 関数の中で player.draw() みたいに呼ばれます。
const draw = () => {
el.style.left = `${$_.x}px`;
el.style.top = `${$_.y}px`;
};
// 各種関数を player オブジェクトに登録します。
$_.reset = reset;
$_.update = update;
$_.draw = draw;
// 最初にオブジェクトが作られるとき、初期化のための仕事 (reset) をします。
console.log('(player) Initializing...');
reset();
return $_;
}
// END OF: createPlayer()
/**
* enemy オブジェクトを作ってくれるファクトリー関数です。
* @param {Object} [el] - "#player" element
* @param {Object} [options={}] - "#btn-stop" element
* @param {Object} [options.canvas] - "canvas" object
* @param {number} [options.step=1] - step(s) per animation frame
* @returns {Object} - "enemy" object
*/
function createEnemy (el, options = {}) {
if (!el) {
throw new Error('No "enemy"');
}
const { canvas, step = 1 } = options;
// enemy の本体となるオブジェクトです。
const $_ = {
el,
x: 0,
y: 0,
alive: true
};
const reset = () => {
let x = $_.x;
let y = $_.y;
x = int(canvas.width / 2);
y = int(canvas.height / 2);
console.log(`(enemy) (${x}, ${y})`);
$_.x = x;
$_.y = y;
};
// ゲームの方の update 関数の中で enemy.update() みたいに呼ばれます。
const update = () => {
if (!inProgress) return;
let x = $_.x;
let y = $_.y;
x -= step;
y -= step;
// ゲーム領域の外に出てしまったらアニメーションを停止します。
if (x < 0 || y < 0) {
if (x < 0) x = 0;
if (y < 0) y = 0;
stopAnime();
}
$_.x = x;
$_.y = y;
};
// END OF: update()
// ゲームの方の update 関数の中で enemy.draw() みたいに呼ばれます。
const draw = () => {
el.style.left = `${$_.x}px`;
el.style.top = `${$_.y}px`;
};
// 各種関数を enemy オブジェクトに登録します。
$_.reset = reset
$_.update = update;
$_.draw = draw;
// 最初にオブジェクトが作られるとき、初期化のための仕事 (reset) をします。
console.log('(enemy) Initializing...');
reset();
return $_;
}
// END OF: createEnemy()
xxxx