7. JS魔法修行: (5) 空間を "隔離" する魔法
「あまり面白味のない」 とお伝えした2つのトピックのうちの一つ、「データの話」 が終わりました。
どうでしたか? 曖昧だったプログラムというものの輪郭が見えてきましたか?
"データの魔法" が使えるようになった今、これを駆使すれば多様なプログラムが書けるようになるはずです。
どうでしたか? 曖昧だったプログラムというものの輪郭が見えてきましたか?
"データの魔法" が使えるようになった今、これを駆使すれば多様なプログラムが書けるようになるはずです。
次に 「もう一つの面白味のない話」 である 「スコープの話」 をします。
こちらは最終的に 自分だけのモジュールを書く ことが目的となります。
「モジュールって何だろう?」 ということもこれから説明していきます。
こちらは最終的に 自分だけのモジュールを書く ことが目的となります。
「モジュールって何だろう?」 ということもこれから説明していきます。
7-1. 他人に迷惑を掛けない
スコープの基本
まずは何の解説もなしに次のコードを見てみましょう。
const pizza = 2;
console.log(pizza); // --> 2
{
const pizza = 100;
console.log(pizza); // --> 100
}
console.log(pizza); // --> 2
3行目を見ましょう。
{ で囲む行が始まります。
これを 「スコープの中に入る」 と表現します。
そしてこれは6行目の } で終わります。
これは 「スコープの外に出る」 と表現します。
{ で囲む行が始まります。
これを 「スコープの中に入る」 と表現します。
そしてこれは6行目の } で終わります。
これは 「スコープの外に出る」 と表現します。
この例では pizza の宣言される箇所が二箇所あります。一つ注意点です。このように 一つのプログラムでおなじ変数名を使うということは通常はあまりやりません。読む人にとって紛らわしいからです。これはチュートリアルだからあえてこうしています。
さて、よく見てみましょう。スコープの中に入って pizza を 100 にしています。でもスコープの外に出てみると、外の pizza は以前とおなじ 2 のままです。
ようするに
スコープの中でやることはスコープの外に影響を与えない
ということです。
ようするに
スコープの中でやることはスコープの外に影響を与えない
ということです。
よくあるスコープの例
実際にあり得そうなプログラムの例を見てみます。
以下の例に x と y というのが何度か出てきます。
それらが 「誰にとっての xy なのか」 を考えてみて下さい。
以下の例に x と y というのが何度か出てきます。
それらが 「誰にとっての xy なのか」 を考えてみて下さい。
const width = 1024; // スクリーンの横幅
const height = 768; // スクリーンの高さ
const x = 10;
const y = 10;
const player = { x, y };
console.log(`The player is at: (${player.x}, ${player.y})`);
// The player is at: (10, 10)
const createEnemy = () => {
// ここの xy は 「敵 (Enemy)」 の xy を計算したいから
// であって、スコープの中だけに通用する xy である。
// 決してスコープの外の xy ではない。
const x = width / 2;
const y = height / 2;
return { x, y };
};
const enemy = createEnemy(); // 「敵 (Enemy)」 の位置を決める
console.log(`The enemy is at: (${enemy.x}, ${enemy.y})`);
// The enemy is at: (512, 384)
理解できますか?
スコープというのは { と } で囲まれたものであれば色々な場面で出現するのですが、これは 「createEnemy という関数の内部に出現するスコープ」 の例です。
スコープの外には確かに x と y が宣言されていますが、createEnemy の内側で使っている x と y は、それとは別のものです。
15 行目を見てみましょう。
const x = width / 2;
const y = height / 2;
return { x, y };
createEnemy 関数の中です。これは "敵" の x と y を計算するものです。最終的に createEnemy 関数から { x: 512, y: 384 } というオブジェクトを返そうとしています。x の方はスクリーンの横幅である 1024 の半分の位置を計算し、y の方はスクリーンの高さである 768 の半分の位置を計算しています。そうすれば "敵" をスクリーンの真ん中に配置できるという寸法です。
これを書いたプログラマにどうして createEnemy の中で x と y を使ったのか聞いてみると 「ちょっとその場で計算したかっただけ」 という返事が返ってきます。つまり 変数名は別に x や y じゃなくてよかった のです。スコープの外にも x と y があるのに 紛らわしい ったらありゃしません。そのことでプログラマを責めると、そのプログラマは "ごめんね" と言って、次のようなコードに書き直したりします。
const enemyX = width / 2;
const enemyY = height / 2;
return {
x: enemyX,
y: enemyY
};
変数名がスコープの外の x と y とは違う名前になったので、紛らわしさがなくなりました。
現実のプログラムではおそらく createEnemy は別ファイル (別モジュール) に書かれているはずで、スコープの外側に x と y など存在しないのが普通です。そうすると紛らわしいことは何もないため、プログラマは自分のプログラムを書き直す必要はなく、おそらく x と y のままだと思います。enemyX とか enemyY などという長ったらしい変数名より、変数名は短いほうが読みやすいですからね。
オブジェクトを返す
ところで上にある例で しれっと流して しまいましたが createEnemy 関数は { x: 512, y: 384 } という 「オブジェクト」 を返しています。これまでの関数は、値を返したとしてもせいぜい数値ぐらいでした。でもここで初めて オブジェクトを返す関数 に出会ったのです。
受け取っている側をみると
const enemy = createEnemy();
console.log(`The enemy is at: (${enemy.x}, ${enemy.y})`);
// The enemy is at: (512, 384)
createEnemy から返される { x: 512, y: 384 } をちゃっかり enemy にセットし、まるで読者の困惑を無視するかのように、しれっと enemy.x と enemy.y でそれぞれの数字を取り出しています。これについて全く説明の無かったことを深くお詫びいたします、、、 JS では 「オブジェクトを返す」 ということを頻繁にやります。
7-2. 隔離された空間をつくる
"聖域" の出現
さて、前回の createEnemy で 「オブジェクトを返す」 ようにしたのには "ある" 意図があります。
これを覚えていますか?
// xonsole というオブジェクト
const xonsole = {
log: s => console.log(s),
warn: s => console.warn(s),
error: s => console.error(s)
};
xonsole.log('boo!'); // --> boo!
オブジェクトの value (値) には、文字列や数値だけじゃなく、「関数」 も入れられるという話でした。
試しにこれをこう書き換えてみます。
const xonsole = createXonsole();
xonsole.log('boo!'); // --> boo!
// オブジェクトを作ってくれる関数
function createXonsole () {
const log = s => console.log(s);
const warn = s => console.warn(s);
const error = s => console.error(s);
return {
log,
warn,
error
};
}
xonsole オブジェクトの中身は前とおなじです。
違うのは createXonsole という関数があることです。
これは オブジェクトを返す関数 です。
これで xonsole を作っているという点が異なります。
違うのは createXonsole という関数があることです。
これは オブジェクトを返す関数 です。
これで xonsole を作っているという点が異なります。
これがどんなふうに便利なのか、例を見ながら考えてみましょう。
// xonsole を作ると同時に name を渡している
const xonsole = createXonsole('Joe');
xonsole.log('You are ugly');
// You are ugly, Joe...
// name を受け取っている
function createXonsole (name) {
// 全てのログ出力に name をつける
const log = s => console.log(`${s}, ${name}...`);
const warn = s => console.warn(`${s}, ${name}...`);
const error = s => console.error(`${s}, ${name}...`);
return {
log,
warn,
error
};
}
xonsole を作るとき、今度は Joe という引数を createXonsole に渡しています。
xonsole.log のほうは以前とおなじ使い方をしています。
でも出力されるメッセージは最後に , Joe... が付くようになりました。
xonsole.log のほうは以前とおなじ使い方をしています。
でも出力されるメッセージは最後に , Joe... が付くようになりました。
じゃあさらに、あまり意味はないですけど x と y を使うようにしてみましょう。
const screen = { width: 1024, height: 768 };
const x = 10;
const y = 10;
// 第一引数は Joe を、第二引数には中に screen が入った {} を渡す
const xonsole = createXonsole('Joe', { screen });
xonsole.log('You are ugly');
// You are ugly, Joe, and you are at (512, 384)
// 第一引数は name という文字列を受け取る
// 第二引数は中に screen の入った {} を options として受け取る
function createXonsole (name, options) => {
// options から screen を取り出す
const { screen } = options;
// スクリーンの真ん中を計算する
const x = screen.width / 2;
const y = screen.height / 2;
// x,y を結合した文字列を作る
const pos = `(${x}, ${y}))`;
const log = s => console.log(`${s}, ${name}, and you are at: ${pos}`);
const warn = s => console.warn(`${s}, ${name}, and you are at: ${pos}`);
const error = s => console.error(`${s}, ${name}, and you are at: ${pos}`);
return {
log,
warn,
error
};
}
スコープの中の x と y は、スコープの外の x と y とは全く無関係であり、プログラマは外のことを気にせず、createXonsole の中で やりたいことを好き放題 にやっています。
この createXonsole という関数のスコープ内部は外から "隔離された空間" です。外からの影響や、また外に対する影響を考えることなく、プログラマは心おきなく xonsole というオブジェクトの制作に専念できるようになります。
せっかちな皆さんのために、結論から言いましょう。
プログラマは 外から隔離されたモジュール空間を作りたい のです。
文脈 (コンテキスト) に準ずる箇所で分断され、一連の流れから独立したスコープは モジュール と呼ばれます。
プログラマは 外から隔離されたモジュール空間を作りたい のです。
文脈 (コンテキスト) に準ずる箇所で分断され、一連の流れから独立したスコープは モジュール と呼ばれます。
厳密にモジュールと呼ばれるのはそれらが別ファイルに保存された場合ですが。
言わば、ここに突如として "聖域" が出現するのです!
function createXonsole () {
// ここに "聖域" が出現する!
// プログラマはこの中で何をやってもいい
// そして最後に自分の作品を返す
return {
...
};
}
これが 多かれ少なかれモジュールというものの "かたち" です。
モジュールの作る方法は色々あるのですが イメージ としてはこれが一番想像しやすい形だと思います。
モジュールの作る方法は色々あるのですが イメージ としてはこれが一番想像しやすい形だと思います。
ここで紹介したモジュールの作り方は 「ファクトリーモデル」 と呼ばれています。そして createXonsole は 「ファクトリー関数」 と呼ばれます。
オブジェクトから中身を取り出す
また脱線します。実は こちらの例でもしれっと流して してしまった箇所がありました。
const xonsole = createXonsole('Joe', { screen });
function createXonsole (name, options) {
// options から screen を取り出す
const { screen } = options;
...
...
createXonsole は第一引数として "Joe" という文字列を name として受け取り、第二引数として { screen } というオブジェクトを options として受け取るというところまではよいでしょう。ところがこのあと、createXonsole はちゃっかり options の中から scope を取り出しています。
これについて説明の無かったことを深くお詫びいたします、、、
これについて説明の無かったことを深くお詫びいたします、、、
通常はこんなふうに
const screen = options.screen;
オブジェクトとその key を . で繋げて value を取り出すわけですが、これは
const { screen } = options;
こんなふうにも書けるよ、という話です。
これは特に説明はないので、"そんなもんだ" と覚えて下さい。
他のプログラミング言語にもよく見られる構文で、これは destructure と呼ばれます。
「聖域」 が必要な理由
どうして "聖域" が必要なのでしょうか?
それを理解するために二つのソースコードを紹介します。
どちらもおなじことをやっています。
どちらもおなじことをやっています。
一つ目はスコープの聖域を 利用しない例。
二つ目はスコープの聖域を 利用する例。
二つ目はスコープの聖域を 利用する例。
プログラムの中身を理解しなくていい ので、今回は次の点からソースコードを観察してみて下さい。
- 流れから独立できそうなモジュール空間があるかどうか
コードが理解しやすいように処理の流れを書いておきます。
- player を作る
- enemy を作る
- player の方は (0, 0) (ブラウザの左上) に配置する
- enemy の方は (512, 384) (ブラウザの中心) に配置する
- update で 0.5 秒 に一回、両方の位置を更新する
- player の x と y は 50 ずつ enemy のほうに向かう (どんどんブラウザの中心へ)
- enemy の x と y は 50 ずつ player のほうに向かう (どんどんブラウザの左上へ)
- player と enemy が交差する位置まで来たら You are dead というメッセージを出す
プログラムはいずれも以下のようなログ出力を行います。
他人のソースコードを読む のは プログラミング言語を学ぶための最高の学習法 です。さっそく読んでいきましょう。
app_07.standard.js
一つ目はスコープの聖域を 利用しない例です。
const screen = { width: 1024, height: 768 };
const step = 50;
const player = { x: 0, y: 0, alive: true };
const enemy = { x: 0, y: 0 };
// ブラウザの左上に配置
player.x = 0;
player.y = 0;
// ブラウザの中心に配置
enemy.x = screen.width / 2;
enemy.y = screen.height / 2;
let timerId = setInterval(update, 500); // ぐるぐる回す
let cnt = 0;
function update () {
console.log(`>>>> ${cnt}`);
// 現在どこにいるかログ出力する
console.log(`player: (${player.x}, ${player.y})`);
console.log(`enemy: (${enemy.x}, ${enemy.y})`);
// player 斜め右下に向かって移動する
player.x += step;
player.y += step;
// enemy は斜め左上に向かって移動する
enemy.x -= step;
enemy.y -= step;
cnt += 1;
// 通り越したらゲームオーバー
if (enemy.x < player.x && enemy.y < player.y) {
player.alive = false;
console.log('You are dead!');
if (timerId) {
clearTimeout(timerId);
timerId = null;
}
}
}
player.x += step だけはちょっと説明が必要ですかね。
これは player.x = player.x + step と同じ意味になります。
これは player.x = player.x + step と同じ意味になります。
app_07.good.js
二つ目はスコープの聖域を 利用する例です。
const screen = { width: 1024, height: 768 };
const player = createPlayer('player', { step: 50 });
const enemy = createEnemy('enemy', { step: 50, screen });
let timerId = setInterval(update, 500); // ぐるぐる回す
let cnt = 0;
function update () {
console.log(`>>>> ${cnt}`);
// 現在どこにいるかログ出力する
player.where();
enemy.where();
player.plus(); // player 斜め右下に向かって移動
enemy.minus(); // enemy は斜め左上に向かって移動
cnt += 1;
// 通り越したらゲームオーバー
if (enemy.x < player.x && enemy.y < player.y) {
player.die('You are dead!');
if (timerId) {
clearTimeout(timerId);
timerId = null;
}
}
}
// これを別ファイルに移動できる
function createPlayer (name, options) {
const { step } = options;
const oo = {
name,
x: 0,
y: 0,
alive: true,
};
oo.plus = () => {
oo.x += step;
oo.y += step;
};
oo.minus = () => {
oo.x -= step;
oo.y -= step;
};
oo.where = () => {
console.log(`${oo.name}: (${oo.x}, ${oo.y})`);
};
oo.die = message => {
oo.alive = false;
console.log(message);
};
return oo;
}
// これを別ファイルに移動できる
function createEnemy (name, options) {
const { step, screen } = options;
const x = screen.width / 2;
const y = screen.height / 2;
const oo = {
name,
x,
y
};
oo.plus = () => {
oo.x += step;
oo.y += step;
};
oo.minus = () => {
oo.x -= step;
oo.y -= step;
};
oo.where = () => {
console.log(`${oo.name}: (${oo.x}, ${oo.y})`);
};
return oo;
}
以上二つのプログラムではその内容を理解する必要はないとお伝えしましたが、モジュールとして独立できそう な箇所は見つけましたか? 明らかに createPlayer と createEnemy はスコープが他から隔離されており、モジュールとして独立できそうです。
実際、現実のアプリケーションでは createPlayer と createEnemy は 別ファイルに分離される ことになるはずです。
そうすると、まず、本体の記述が短くなる という利点があります。また createPlayer と createEnemy で作られたオブジェクトたちには、plus, minus, where, die という関数群が外向けの 「インターフェース (お客様窓口)」 として用意され、使う側からすれば、中で何をやっているか意識せずに plus や die などといった関数の 名前 から "察して" これらを利用できるようになります。
このチュートリアルでは繰り返し 名前から察するのがプログラミングの全てだ とお伝えしてきました。みなさんがご自分のプログラムを他の開発者に提供するとき、実質的にそのプログラムはインターフェースとして外向けの関数たちを内包するオブジェクトを返します。そのとき他の開発者が理解しやすいような "関数名" を決めることは非常に重要です。
この章で学んだ "スコープ" は、外の世界から隔離されたモジュール空間をデザインするのに必要となる知識でした。そしてプログラムと他のプログラムとのやり取りは 関数に渡すもの と 関数から返されるもの 、そしてその "データのかたち" に依存することを学びました。JS に限らず、これは全てのプログラミング言語に共通するプログラムの仕組みです。
次の章では、これまでサンプルとして書いてきたプログラムたちを "より実用的なもの" (まあ、ゲームでしょうね・・・) へと再構築していきます。
残念ながらモジュールを別ファイルに分離して利用することは、現段階のチュートリアルの知識では出来ません。ES Modules という仕様が用意されており、別ファイルのモジュールを読み込む記述は簡単なのですが、そのためにはご自身のパソコンでサーバを立ち上げる必要があります。そうしないと最新のブラウザではセキュリティ違反で怒られてしまうのです。