JavaScriptで学ぶ 文系の人にもわかるプログラミング入門
第7章 配列とグローバル変数
変数, 演算子, 関数, 分岐(if ... else), 繰り返し(while ..., for ...)など, 多くの概念が登場しましたが, まだとても重要なものがひとつ残っています。 ごくまれな例外を除いて, ほとんどすべてのプログラミング言語に備わっているのが「配列(array)」です。 この章では, この「配列」を使って, 「計算練習」をさらに発展させていきましょう。
同じ問題は出したくない ── 配列に記憶
前の章の最後で, 採点まで表示することになり, だいぶ計算ドリルらしくなりましたが, まだまだ改良の余地があります。 まず気になるのが, 1回のドリル(たとえば20問)の中で, 同じ問題が出題されることです。 前にまちがっていたので, もう1回同じのを出すのなら話は別ですが, 簡単にできたのに同じのを出すのは止めたいところです。
この問題を解決するには, それより前に出した問題を記憶しておく必要があります。 そのために配列を使いましょう。
プログラム7-1 example07-01.html 計算問題8
関数keisan()は第6章の最後のバージョンとほとんど同じで, confirm()をalert()にしただけですが, tashizanHikizan()に少し変更が加わり, 新しくmondaiChofuku()という関数が加わっています。
tashizanHikizan()は, 問題を1個生成したらすぐにそれを実行するのでなく, 関数mondaiChofuku()を呼んで生成した問題がすでに出した問題と同じでないかを確認して, 同じでない場合のみ, その問題を表示するように変わっています。 このため, 新たにdo ... whileという形式の繰り返し(ループ)が加わりました。
do ... whileループの形式は次のとおりです。
do { <処理部> } while (<再実行の条件>);
ともかく1回は処理部を実行して, 2回目以降は<再実行の条件>が成り立つ場合(真の場合)にのみ<処理部>を実行します。 この例の場合, 「ともかくひとつ問題を生成してみて, それがすでに前に出された問題と同じならば再度実行する, そうでなければループを抜ける」という処理をします。
下請けのmondaiChofuku()はtashizanHikizan()からmondai(問題)を受け取ってそれがすでに出した問題と重複していないかをチェックし, true(重複している)あるいはfalse(重複していない新しい問題)を戻すことになっています。
グローバル変数(大域変数)とローカル変数(局所変数)
では, 下請けの関数mondaiChofuku()を見ましょう。 mondaiChofuku()のほうに「配列」が関係しています。 まず, 73-74の2行に注目してください。
73 var MondaiKioku = new Array(); 74 var MondaiBangou = 0;
これまでの変数はすべてfunction xxxx {...} の...の範囲に入っていたのに, この2行はどの関数にも属していません。 このように変数を宣言すると, 「大域変数(グローバル変数, global variable)」を宣言したことになります。 今まで使ってきた変数は, 関数の本体を表す{...}の...の部分に書かれており, この場合その関数内部でしかその変数にアクセスすること(その変数の値を見たり, その変数に値を代入したりすること)はできませんでした。 関数内部でのみ有効な「局所変数(ローカル変数, local variable)」だったのです。
ローカル変数とグローバル変数の大きな違いはもうひとつあります。 グローバル変数は, プログラムの実行の間ずっと存在し続け, 指定された値を記憶し続けます。 これに対して, ある関数内で使われたローカル変数はその関数の実行が行われている場合のみ有効で, しかも記憶している内容もその関数の実行が終わると消えてしまいます。
tashizanHikizan()の冒頭で宣言されているno1, no2, mondaiなどの変数はいずれもtashizanHikizan()の内部でのみ使うことができ, しかもtashizanHikizan()が呼び出されるたびに新しく作られるので, 前回使われたときに記憶していた値はすべて忘れてしまいます。 これに対して, MondaiKiokuとMondaiBangouはグローバル変数なので, この2つの変数に記憶されている値はプログラムの実行中ずっと存在し続けるのです。
このプログラムでは, グローバル変数の「指定された値を記憶し続ける」性質を利用して以前に出された問題を記憶しておくことにします。 ローカル変数でこのようなことをするのは簡単ではありません。
メモ
じつは, このような処理に対してグローバル変数を使う手法は, 最近はあまり推奨されなくなっています。 その代わりによく使われるのが「オブジェクト」を使う手法です。 しかし, グローバル変数を使う人もまだまだたくさんいますし, 以前に書かれたプログラムの保守や修正を行う場合もあるでしょうから, ここでそのような手法を学んでおいてけっして無駄にはなりません。 また, 自分用にちょっとしたプログラムを作る場合は, グローバル変数を使ってしまった方が楽な場合があるのも, これまた事実です。
メモ
変数が初めて使われるとき, コンピュータのメモリ内でその変数に入る値を記憶するのに必要な場所(「記憶領域」あるいは「記憶域」)が確保されます。 ローカル変数の場合は, その領域は確保されるたびに変わることになります。 関数で使われる変数は, 関数が呼び出されるたびに通常は前の時とは別の場所に確保されます。 グローバル変数の記憶域はプログラムの実行の最初から最後まで同じ場所になります。
この場所のことをアドレス(address, 番地)と呼びます。 例で使われているグローバル変数MondaiKiokuは, たとえば320番地に置かれたとしたら, このアドレスはプログラムの実行中ずっと変わりません。 これに対して, tashizanHikizan()のmondaiはある時は16000番地に, またある時は19200番地にといった具合に割り当てられるアドレスが異なります。
メモ
関数内でグローバル変数と同じ名前のローカル変数を宣言したらどうなるでしょうか? たとえば, mondaiBangouというグローバル変数と同じ名前の変数をtashizanHikizan()でも次のように宣言したとします。
var mondaiBangou; // グローバル変数の宣言 function tashizanHikizan() { var mondaiBangou; // ローカル変数の宣言 ... mondaiBangou = ...; // ローカル変数が使われる }
この場合は, ローカル変数のmondaiBangouが優先され, グローバル変数にはアクセスできなくなってしまいます。
しかし, 一番よいのはそのようなローカル変数を使わないことです。 同じ名前をもつのならば, 同じ役割をするはずですから, あえて新しいローカル変数を使うのは変でしょう。 紛らわしいことは避けるに越したことはないのです。
メモ
前に「JavaScriptでは, 変数は宣言せずに使ってよいことになっている」と書きましたが, 変数を宣言せずに用いると, たとえその関数でしか使わない変数でも, グローバル変数になってしまいます(ほかの多くの言語ではこのようなことはありません)。 こうすると, プログラム中のどこからもアクセスできるようになってしまうため, 誤ってその変数の値を変更してしまうなどの危険が生じます。 したがって, この意味からも, 変数はいつも宣言してから使うのを習慣にすることをおすすめします。
たくさんの値をまとめて記憶する ── 配列
出した問題を次々と記憶していかなければならないので, そのためにかなりの数の変数が必要になります。 そのための機構が「配列」です。
74行目は配列の宣言で, MondaiKiokuという配列を新たに作成しています。 実際にこの配列を使っているのは78行目からのfor文の中です。
74 var MondaiKioku = new Array(); 75 var MondaiBangou = 0; 76 77 function mondaiChofuku(mondai) { 78 for (var i=0; i<MondaiBangou; i++) { 79 if (MondaiKioku[i] == mondai) { 80 return true; 81 } 82 } 83 MondaiKioku[MondaiBangou] = mondai; 84 MondaiBangou++; 85 return false; 86 }
もうひとつのグローバル変数MondaiBangouはこれまでにいくつの問題を出したかを記憶するためのものです。 MondaiBangouの初期値は0(75行目)で, 新たな問題ができるごとに84行目で1プラスしています。 したがって, 上のfor文の前ではこれまでに出した問題の数がMondaiBangouに入っています。
for文の最初でまず変数iを宣言して0に初期化し, iがMondaiBangouよりも小さいかがチェックされます。 一番最初にこの関数が呼ばれたときは, MondaiBangouの値は0で, iも0なのでこの条件は成り立たず(真にならず), forの<処理部>は一度も実行されずに下に行ってしまいます。 つまり, まだ問題は1個も出してないので, 前に出した問題と比較する必要はないというわけです。
for文に続く次の行が実行されるとき, MondaiBangouは0になって, 変数mondaiにこの上で生成した問題が文字列として入っています。
83 MondaiKioku[MondaiBangou] = mondai;
このときには次の文が実行されるのと同じことになります。
MondaiKioku[0] = mondai;
つまり, MondaiKiokuという配列の0番目の要素にmondaiの値が記憶されます。 プログラミングの世界ではいろいろなものが1からではなく, 0から始まりますが, 配列の「添字(そえじ, index)」も通常は0から始まります(プログラミング言語によっては配列の添字が1から始まるものや, 最初の添字の番号を指定できるものもあります)。
配列MondaiKiokuの先頭(MondaiKioku[0])に第1問を記憶した後, 次の文でMondaiBangouの値を1増やし, 第1問の生成が終わってこの問題を記憶した事実を覚えておきます。
84 MondaiBangou++;
問題1はMondaiKioku[0]に, 問題2はMondaiKioku[1]に, 問題3はMondaiKioku[2]に, ...とひとつずれて記憶されていることに注意してください。
最後に, 呼び出した関数にfalseを返していますが, これは生成された問題はすでに出された問題ではありません(重複していない)ことを表しています。
84 return false;
この関数mondaiChofuku()が2度目に呼ばれたときはどうなるでしょうか。 すでにMondaiKioku[0]に最初の問題が記憶されており, MondaiBangouには1が入っています。 78行目のfor文は, iが0のとき「i<MondaiBangou」がtrueになるので, このfor文の79〜81行目の<処理部>が実行されることになります。
78 for (i=0; i<MondaiBangou; i++) { 79 if (MondaiKioku[i] == mondai) { 80 return true; 81 } 82 }
MondaiKioku[0]が呼び出し側から渡されたmondaiと比較されて, 同じならば80行目でtrueを戻してこの関数を抜けます。
MondaiKioku[0]がmondaiと違う場合は, i++を実行します。 そして, i<MondaiBangouのチェックが行われ, これはiもMondaiBangouも1になるため, falseになり, for文の本体部分は実行されずにその下(83行目以降)が実行されます。 その結果
83 MondaiKioku[MondaiBangou] = mondai;
は
MondaiKioku[1] = mondai;
として実行され, 配列MondaiKiokuの2番目の要素(添字は1)に呼び出し側から渡された問題が記憶されます。 その下の行でMondaiBangouが1増えそしてfalse(問題は重複していなかったことを示す)を戻します。
このように, mondaiChofuku()の中のfor文ではすでに生成された問題と同じかどうかがチェックされ, 同じ場合はtrueを返して終了します。 同じ問題が配列MondaiKiokuのどこにもなかったら, これは新しい問題なのでMondaiBangouを1増やしてfalseを返して終了します。
メモ
forループなどループはぐるぐる同じところを回るので, どういう処理になっているのか(とくにほかの人が書いたプログラムを読むときには)混乱してしまうことがあります。 そのような場合, 上で見たように, まず1回目のループでは各変数がどう変化するか, 分岐がある場合はどの道を通るかを見て, 続いて2回目のループではどうなるか, 3回目のループではどうなるか, といった具合に順番に変数の変化や処理の順序を追ってみることをお勧めします。
これで, 全体の動作をひと通り追ったことになります。 もう一度まとめると, 次のようにして同じ問題を出すのを避けているわけです。
- グローバル変数の配列を使って, それまでに出した問題を記憶しておく。
- 問題を生成時に, 配列に記憶されている要素すべてとの比較により, すでに出した問題かどうかをチェックする。 このときループ(for文)を使う。
- 同じ問題だった場合は, もう一度生成して同じようにチェックする。 これをまったく新しい問題が生成されるまで繰り返す。
この例のように, 配列を使うとたくさんの値をまとめて記憶することができます。 配列もプログラミングにおいて非常によく使われるものです。
まとめ
この章では配列を使って, いくつもの要素を記憶する方法を説明しました。 また, グローバル変数(大域変数)とローカル変数(局所変数)についても説明しました。
プログラミング一般
- 配列
- グローバル変数(大域変数)とローカル変数(局所変数)
JavaScriptの構文
-
do ... while文 —— 最低1回は処理部を実行するループ
do { <処理部> while (<再実行の条件>);
JavaScriptで学ぶ 文系の人にもわかるプログラミング入門
第7章 次の章へ