新しめ(といってもオブジェクト指向という言葉も登場してだいぶ経つ)のプログラミング言語には、オブ ジェクト指向の機能を取り入れたものが多い。
ではそのオブジェクト指向というのを使うと、何がどう便利なのだろう??
ここで作っているのは物理シミュレーションなので、物理シミュレーションを作る時の必要性から考えてみよう。
物理シミュレーションを行うということは例えば物体の運動をシミュレートするわけだが、そのとき我々は
などなどを知らなくてはいけない。
オブジェクト指向のための機能を持たない言語で「物体Xの運動」をプログラムしようとすると、上のそれぞれのために、
と、せっせと変数を作っていく必要がある。
↓のような感じにだ。
var x; var y; var vx; var vy; var m; var q;
しかし、これらの変数がたくさん出て来ると、面倒なことが沢山出て来る。例えばこの物体の位置を計算する「関数」があって(しかも物体の位置を計算するには現在位置x,yと速度vx,vyと質量と電荷、そしてもちろん現在時刻を全部知る必要があったりすると)その関数を呼び出すには
calcPostion(x, y, vx, vy, m, q,t);
のように全ての属性(最期のをその関数(calcPosition)に渡してやらなくてはいけない。
という願望が出てくる。
たとえば、「物体X」を表す、その名も「X」という変数だけを、
calcPositon(X,t);
と渡せばいいなら楽ではないか(時間は物体の属性ではないから別である)。
この、「ある物体の持つ属性はひとまとめにする」いやむしろ、「変数Xが物体そのものを表現している」ようにしよう、というのがオブジェクト指向の第一歩となる。
「物体X」しか出てこないのならいいが、物理シミュレーションを作っていれば2個めの物体を出したくなる。「物体Y」も出て来ると、
のように物体X用の変数を用意した後で、物体Y向けに
と、同じようにせっせとまた変数を一組作らなくてはいけない(物体Xのx座標も物体Yのx座標もどっちもxと いう変数にしてしまったら、プログラムはまともに動かない)。
さらに、たとえば物体Xが等速直線運動しているのだとすると、
X_x += X_vx*t; X_y += X_vy*t;
のようなコードを書きたくなる。
これも、Yに対してはまた、
Y_x += Y_vx*t; Y_y += Y_vy*t;
と書き、次にZという別の変数が現れたらまた、
Z_x += Z_vx*t; Z_y += Z_vy*t;
と何度も書くはめになる…のはなんとも面倒くさい。
という願望を叶える方法としても、オブジェクト指向は使える。
自分一人でプログラムしている時でも同じである。「半年前の(下手すると一週間前でも)自分は赤の他人だ」 ということは心得ておこう。
オブジェクト指向の利点の一つ(まだまだ一つ目だ)は、この願望を叶えてくれることだ。では、オブジェクト指向言語が使っている解決法を以下で説明していこう。
多くのオブジェクト指向言語では、Xという変数オブジェクトに
X.x X.y X.vx X.vy
のように記号「.」(ピリオド)をつけてその変数の属性(プロパティと呼ぶ)を持たせることができる。
それぞれのプロパティには数字や文字列を代入したり、その数値を使って計算したりできるので、
X.x = X.vx * t
のように書くことができる。
たとえば、
function plus_vt(X,t) { X.x += X.vx*t; X.y += X.vy*t; }
のような関数を一回書いておけば、
plus_vt(Y,t); plus_vt(Z,t);
と書くことでYやZに対してこの計算をやってくれる(当然、YやZはxやらvxやらのプロパティを持ったオブジェクトでないとエラーが出る)。
プロパティという機能がなかったとしても、
function plus_vt(x,y,vx,vy,t) { x += vx*t; y += vy*t; }
のようにすれば、と思うかもしれないが、これだと
plus_vt(Y_x,Y_y,Y_vx,Y_vy,t);
のように冗長な書き方をしなくてはいけない。
ここまでで、物体(オブジェクト)を表す変数が属性を持っていればいろいろ便利だ、ということを説明してきたが、さらに変数だけでははなくfunction(「関数」と訳すのが普通だが、ここは「機能」と訳す方が実態に近いかもしれない)も物体(オブジェクト)に持たせる。オブジェクト指向言語の多くでは「メソッド」とか「メンバ関数」などという方法でそれを実現するのだが、javascriptは「プロパティに関数を持たせる」という方法でこれを実現する。
X.plus_vt=function(t) { this.x += this.vx * t; this.y += this.vy * t; }
と定義しておけば、
X.plus_vt(0.1);
と呼び出すことで0.1秒の時間経過を起こしてくれる。
しかし、これではYやZに対してはまたコードを書かなくてはいけなくなり、不経済である。
どうすればよいか、ここで使っているサンプルプログラムでも使われている手法で説明しよう。
オブジェクト指向プログラミングでは、同じ種類のオブジェクトを作るときには、まずそのオブジェクトの「型」である「クラス」を作り、その「クラス」に属するオブジェクトを作る。「クラス」はいわばオブジェクトの鋳型である。
例として、Vectorというクラスを考えよう。名前の通りベクトル(今考えているシミュレーションは2次元の物理現象だから、2次元ベクトル)である。まずはvector.jsの最初にあるVectorというクラスの定義を見てみよう。
// 2次元ベクトル var Vector = function (xx, yy) { this.x = xx; this.y = yy; };
まずこうして「Vector」というのが定義されていると、この後、a=new Vector(3,5);のようにnewという命令でVectorクラスを作ることができる(その新しいベクトルの変数の名前が、ここではa)。以後、a.xがaのx成分、a.yがaのy成分で、最初の、a=new Vector(3,5);によりx成分は3に、y成分は5になっている(上に引用したコードのおかげ)。
さて、ベクトルが定義できたら、そのベクトルを足したり引いたり内積をとったり外積をとったりしたくなるだろう。それも一度書いておけば後は再利用できるようにしておく。そのためには、
Vector.prototype = { copyFrom: function (V) {this.x = V.x; this.y = V.y; }, // v.copyFrom(v2)で、vがv2のコピーとなる。 // 【ベクトルなどの代入に関する注意】 // javascriptではプリミティブ型(整数や実数など)以外の変数(オブジェクト)に対しては、 // = による代入はコピーではなく、一つの実体を共有することになる。 // たとえば(Vectorはプリミティブじゃないから)、v = v2; のようにすると、 // vとv2が「同一物の別名」のようになってしまい、 // 代入後にどちらかを変化させるともう一方も変化してしまう(実体が一つしかないと思えば当然だ)。 // v = copyFrom(v2); ならそうならない。 makeCopy: function () { return new Vector(this.x, this.y); }, // v2 = v.makeCopy()で、vがv2のコピーとなる。 // ベクトルを引数にして関数を呼び出すとき、呼び出す関数にそのベクトルを変更してほしくないときに、 // f(v.makeCopy());のようにして呼び出すとよい。 setXY: function (xx, yy) { this.x = xx; this.y = yy; }, // v.add(v1)で、v = v+v1 add: function (V) {this.x += V.x; this.y += V.y; }, // v.sub(v1)で、v = v-v1 sub: function (V) {this.x -= V.x; this.y -= V.y; }, // w = v.add(v1)で、w = v+v1
途中省略
rot90: function () { var newx = -this.y; this.y = this.x; this.x = newx; }, rot270: function () { var newx = this.y; this.y = -this.x; this.x = newx; } };
のように「プロトタイプ(prototype)」を定義する。
Vector.protypeを設定しておくと、以後Vectorが作られるとこのプロトタイプを参照して、「自分が何をすべきか」ということを決めてくれる。たとえばこの中にadd: function (V) {this.x += V.x; this.y += V.y; },という部分があるが、これによって、Vector.prototype.addが、function (V) {this.x += V.x; this.y += V.y; }という関数になっている。これのおかげで、この後a.add(b);とやると、a.x += b.x; a.y += b.y;に対応するコードが実行される。
ここででてきたthis.というのは文字通り「これ」つまり「自分自身」を意味していて、addという関数がa.add(b);と呼び出されたならばthisはaであり、b.add(c);と呼び出されたならthisはbである。
Vectorという「クラス」が何を知っているか(何ができるか)はソースを眺めてみて欲しい。上の引用部分の最後のrot90、rot270は90度と270度の回転である(a.rot90();でベクトルaが90度回転する)。