ホーム < ゲームつくろー! < IKD備忘録

JavaScript
JavaScriptでのクラスの作り方まとめ


 JavaScriptでのクラスの作り方をまとめてみました。


@ クラス宣言

 JavaScriptにはクラスという言語仕様は無いので模倣して作ります。クラスを宣言するにはグローバルな領域にクラス名な変数を定義し、その変数に無名関数を代入します:

var Vector3 = function() {
};

 このクラスのインスタンスを作るには「new演算子」を用います:

var v = new Vector3();

new演算子はその後ろにあるオブジェクトのインスタンスのようなものを作ります。では次の4つは何が違うのか?

var v1 = Vector3;
var v2 = Vector3();
var v3 = new Vector3;
var v4 = new Vector3();

まずv1は単なるコピーなのでVector3そのもの、つまり上で定義した関数と同じになります。
v2はVector3()と関数の呼び出しの戻り値を取得している状態。いま関数は戻り値を定義していないので「undefined」になります。
v3はVector3をnewしています。これによってv3には「Vector3のプロトタイプ」がコピーされます。プロトタイプについては後述します。この宣言をすると実は勝手に「プロトタイプのコンストラクタ」が呼ばれます。先の無名関数の代入はプロトタイプのコンストラクタを上書きした状態になっているため、v3のような宣言をしても関数がちゃんと呼ばれます。
v4も実はVector3をnewしています。v3との違いは生成時に上書きされたコンストラクタに引数を渡せるという事です。上の無名関数は引数がありませんが、これを引数付きにする事もできます。その時にv4の指定が役に立つわけです。

 プロトタイプというのはオブジェクトが持つ暗黙の値の事で、new演算子でコピーされる対象です。上の無名関数であるVector3は、この状態だと唯一自分自身(function())をプロトタイプの値として持っています。そのため、new演算子でプロトタイプをコピーするとこの関数もコピーされる事になります。


A メンバ変数

 メンバ変数は上の無名関数の中で次のように宣言します:

var Vector3 = function() {
    this.x = 0.0;
    this.y = 0.0;
    this.z = 0.0;
};

 最初これ「?」と思ったのですが、thisの意味を知って納得。thisは関数の中でのみ使える物で「私を保持している人の値を使う」という意味を持ちます。この無名関数をVector3に代入すると、これはVector3自身の持ち物になります。つまり、「私を保持している人」がVector3になるという事です。ただ、上の実装でVector3自身はまだxyzを持っていないはず。そういう場合、JavaScriptは勝手にxyzを作ってしまいます。なので、newを使用してこのコンストラクタが実行された時に、Vector3にxyzが動的に付属する事になるわけです。



B メソッド

 メソッドを定義する方法は色々とあるのですが、概念的にもわかりやすいのは次のように配列で定義する事かなと思います:

var Vector3 = function() {
    this.x = 0.0;
    this.y = 0.0;
    this.z = 0.0;
};

Vector3.prototype = {
    getX: function() {
        return x;
    },
    set: function(x, y, z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
};

var Vector3と定義した段階ではプロトタイプの中身は空っぽです(コンストラクタは特別)。そこにgetXやsetなどをキーとする連想配列として一気に登録しているわけです。キーに対する値は関数になっているので、

var v = new Vector3();

var x = v.getX();

とすると関数が呼ばれてxの値が取られる事になります。ここでポイントは「prototype」に定義された関数が呼ばれているという点。上のgetXは「x」を戻しています。thisが無いのですが大丈夫なのか?というと大丈夫です。prototype内の関数はそれを保持している人が持っている変数を参照しに行こうとするためです。set関数は引数と区別するため明示的にthisを付けています。


C static変数

 static変数(クラス変数)は特殊な宣言をすると作れます:

var Vector3 = function() {
    this.x = 0.0;
    this.y = 0.0;
    this.z = 0.0;

    arguments.callee.g_val = 100.0;  // static変数
};

arguments.calleeというのはfunction自身の属性値のような物を指定する場所で、そこにg_valという適当な変数名を付け加えると属性を追加する事ができます。今Vector3自身は関数なので、そこにg_valという変数がくっついた状態になりました。このstatic変数にアクセスするには、

var sx = Vector3.g_val;

とクラス名(本当は関数そのもの)で直接g_valを指定します。ちなみに、

var v = new Vector3();
var sx = v.g_val;  // Error!

このようにインスタンスからg_valを触ろうとしてもエラーになります。理由は明らか。vはVector3のプロトタイプをコピーしたもので「関数その物」では無いからです。

 プロトタイプに代入したメソッド内からstatic変数にアクセスする場合は、

Vector3.prototype = {
    getX: function() {
        return x + Vector3.g_val;
    }
};

です。ここもg_valとアクセスは出来ません。



D staticメソッド

 staticメソッドはこんな感じでコンストラクタ内に定義します:

var Vector3 = function() {
    this.x = 0.0;
    this.y = 0.0;
    this.z = 0.0;

    arguments.callee.g_val = 100.0;  // static変数
    arguments.callee.getGlobalVal = function() {
        return Vector3.g_val;
    }
};

ここ、大きなポイントです。関数自身の属性値にしたいのでarguments.calleeに追加するのはわかります。で、この関数はstatic変数であるg_valを返すようにしているのですが、Vector3.g_valとしています。まず、これは大丈夫なのか?というと、もちろん大丈夫。理由はgetGlobalVal関数を呼ぶにはVector3.getGlobalVal()と呼ぶ必要があり、Vector3の存在が保障されるから。次に戻す値はarguments.callee.g_valじゃないのか?という疑問も湧きますが、これはVector3.g_valにしないといけません。なぜなら、arguments.callee.g_valとすると「getGlobalVal関数の中のg_val」を見ようとしてしまうため。getGlobalVal関数内ではg_valを定義していませんよね。なのでundefinedが返ってしまいます。

 staticメソッドにアクセスする方法はstatic変数と同じでクラス名で直接関数を指定します:

var sx = Vector3.getGlobalVal();



E 継承

 継承も色々とやり方があるようなのですが、見た目に意味合いが分かりやすい方法を取る事にします。子クラス(Vector4)のコンストラクタを定義した後に、その子クラスのプロトタイプにに親クラス(Vector3)のプロトタイプをそっくりコピーしてしまいます。後はメソッドを追加するもよし、親のメソッドを上書きするもよしです:

var Vector4 = function() {
    this.w = 30.0;
    x = 20.0;
}
Vector4.prototype = new Vector3;

Vector4.prototype.getW = function() {
    return this.w;
}

var vec4 = new Vector4();
var vec4X = vec4.getX();
var vec4W = vec4.getW();

Vector4を定義した段階でメンバ変数wが追加されて、元のメンバxがVector4用に20.0に上書きされます。コンストラクタを抜けた段階でそのプロトタイプは空っぽ。そこにnew演算子でVector3のプロトタイプを直ちに流し込んで、さらにprototype.getWという関数を追加しています。関数内ではthis.wとアクセスするのを忘れずにです。これでVector3の機能を継承したVector4の出来あがりです。

 ちなみに、JavaScriptは型定義がありませんから、C++にある「親ポインタに子ポインタを代入できる」というお話は最初からありません。親だろうと子だろうと別人だろうと、同じ変数に何でも突っ込めちゃうわけです。便利というか怖いというか、ほほほ(^-^;