JavaScriptでクラスを実現する方法

JavaScriptはクラスをサポートしないため、ここで解説するのは正確な意味でのクラスとは異なります。

クラス定義 (functionキーワード)

functionキーワードを用いて定義します。これは通常の関数を定義する構文と同一です。

// クラス (コンストラクタ)
function Foo()
{
    // プロパティ
    this.x = 10;
}

このように定義したクラスは、new演算子でインスタンス化できます。

var foo = new Foo();

メソッド (prototypeプロパティ)

クラスのprototypeプロパティに、関数リテラルの構文で定義します。ちなみにこのprototypeプロパティは、Objectクラスから継承されるものです。prototype - JavaScript | MDN

function Foo() {}
Foo.prototype.A = function() {}

またはオブジェクトリテラルの構文を使用することで、複数のメソッドを続けて定義できます。

Foo.prototype =
{
    A : function() {},
    B : function() {}
};

ただしオブジェクトリテラルを使用できるのは、新規にメソッドを定義する場合のみです。たとえば、

function Foo() {}

Foo.prototype =
{
    A : function() {},
    B : function() {}
};

Foo.prototype = // ここで上書きされる
{
    C : function() {}
};

のようにprototypeプロパティに追加しようとすると、既存のプロパティに上書きされるために先の定義が失われてしまいます。これはオブジェクトのプロパティを定義することを考えれば、容易に理解できることです。

なおprototypeプロパティを省いて定義するとクラスメソッドとなり、まったく違う意味を持ちます。

組み込みオブジェクトへのメソッドの追加

prototypeプロパティを利用することで、NumberStringといった組み込みオブジェクトにメソッドを追加できます。たとえばNumberにメソッドを追加するには、

Number.prototype.toHex = function()
{
    return this.toString( 16 );
}

のようにします。メソッドを新規に作成するわけではないため、オブジェクトリテラルの構文は使用できません

この方法は便利ではありますが、軽率に使用するとコードの保守性を低下させるため、不用意に使用すべきではありません。適した使い方は、標準に準拠しないJavaScriptの実装に、標準に準拠したメソッドを追加するような場合です。

簡易的なメソッドの定義方法

プロパティのように、thisキーワードでメソッドを定義することでも可能です。

var Foo = function()
{
    this.A = function() { return 'A'; } // メソッド
    this.x = 1;                         // プロパティ


    var B = function() { return 'B'; } // ローカルスコープの関数
    C = function() { return 'C'; }     // グローバルスコープの関数

    function D() {  return 'D'; }      // ローカルスコープの関数
}

var foo = new Foo();
alert( foo.A() ); // OK: 'A'が返される
alert( foo.x );   // OK: 1が返される


alert( foo.B() ); // TypeError: foo.B is not a function
alert( foo.C() ); // TypeError: foo.C is not a function
alert( foo.D() ); // TypeError: foo.D is not a function

alert( B() ); // ReferenceError: B is not defined
alert( C() ); // OK: 'C'が返される
alert( D() ); // ReferenceError: D is not defined

ただしこの方法では、クラスをインスタンス化するたびにメソッドのコピーも作成されるため、メモリが無駄に消費されます。

プロパティ

プロパティの定義方法はメソッドと同様ですが、関数以外のデータ型を用いるとそれがプロパティと見なされます。プロパティへのアクセスは、コンストラクタまたはメソッドからはthisを介して行います。一方でクラス外からは、クラスのインスタンスを介します。

function Foo()
{
    this.x = 1;
}

Foo.prototype.A = function()
{
    // メソッド内からのプロパティへのアクセス
    this.x++;
}


var foo = new Foo();

// インスタンスからのプロパティへのアクセス
foo.x++;

thisキーワード

関数内でのthisの意味は、その関数の呼び出され方によって異なります

  • メソッドとして呼ばれた場合 … メソッドを呼び出したインスタンスへの参照
  • 関数として呼ばれた場合 … グローバルオブジェクトへの参照

またイベントハンドラ内でのthisの意味は実装によって異なりますが、基本的にイベントを発生したインスタンスへの参照となります。

メソッド内の関数

以下のコードで示すように、メソッド内から関数として呼ばれた関数内では、thisはグローバルオブジェクトを参照します。そのためクラスのプロパティにはアクセスできません

function Foo()
{
    this.x = 1;
}

Foo.prototype.A = function()
{
    // メソッドとして呼ばれているので、
    // thisはfooを参照する
    alert( this.x ); // 1と表示

    // メソッド内に入れ子にされた関数
    function B()
    {
        // 関数として呼ばれているので、
        // thisはグローバルオブジェクトを参照する
        alert( this.x ); // undefinedと表示
    }

    B(); // 関数の呼び出し
}

var foo = new Foo();
foo.A(); // メソッドの呼び出し
オブジェクトのプロパティによって参照されている関数
var x = 1;       // グローバル変数

var foo = {
    x:2,         // オブジェクトのプロパティ
    A:function() //
    {
        alert( this.x );
    }
};

alert( x );     // 1と表示 (グローバル変数)
alert( foo.x ); // 2と表示 (オブジェクトのプロパティ)


foo.A();        // 2と表示 (Aはオブジェクトのスコープで呼ばれる)

// オブジェクトが参照している関数をコピーする
var bar = foo.A;

bar();          // 1と表示 (barはグローバルのスコープで呼ばれる)

以上のことから関数内でthisを使用する場合には、その関数の呼ばれ方に留意する必要があります。

参考

変数を初期値とするプロパティ

プロパティに基本型の変数を指定した場合、そのクラスがインスタンス化された時点の変数の値で初期化されます。

var num = 1;       // 基本型 (数値)
var obj = { a: 1 }; // 参照型 (オブジェクト)

function Foo()
{
    this.x = num;
    this.y = obj;
}

var foo = new Foo();

alert( foo.x );   // 1
alert( foo.y.a ); // 1

// プロパティの初期値としている変数の値を変更する
num = 2;
obj.a = 2;

alert( foo.x );   // 1 … 影響なし
alert( foo.y.a ); // 2

これを、基本型の変数でもその実体に直接アクセスできるようにするには、ゲッターやセッターを用います。

コンストラクタとデストラクタ

コンストラクタ

クラス定義を行った関数が、コンストラクタとなります。コンストラクタの戻り値は意味を持たないため、returnは記述しないようにします。

// クラス (コンストラクタ)
function Foo()
{
    // returnを記述しない
}

なおオブジェクトのコンストラクタは、constructorプロパティで参照できます。

var foo = new Foo();
alert( foo.constructor == Foo ); // true

引数付きコンストラクタ

引数を持つ関数のように定義することで、引数付きコンストラクタとなります。

function Foo( a )
{
    this.x = a;
}

デストラクタ

JavaScriptでは、言語仕様としてのデストラクタのサポートはありません

終了処理を行う必要があるならばそれを行うメソッドを定義し、そのメソッドを明示的に呼び出すようにします。

function Foo() {}
Foo.prototype.Destructor = function() // デストラクタのつもり
{
    // 終了処理
}

var foo = new Foo();

...

foo.Destructor(); // デストラクタの呼び出し

静的メンバ

これらはインスタンスではなく、クラスそのものに関連付けられたメンバです。C++を例にするならば、static属性を付けた静的メンバに相当します。

定義方法は、オブジェクトのプロパティのそれと同一です。

クラスメソッド

コンストラクタのプロパティとして関数を設定すると、それがクラスメソッドとなります。またクラスメソッド内でのthisキーワードは、そのクラスのインスタンスではなくコンストラクタ関数を参照します。

function Foo()
{
    this.x = 1; // プロパティ
}

Foo.x = 2;      // クラスプロパティ


// クラスメソッドの定義
Foo.A = function()
{
    return this.x; // クラスプロパティを参照するため、2が返される
}

// クラスメソッドの呼び出し
Foo.A();

メソッド内からのメソッドの呼び出し

function Foo() {}

Foo.prototype.B = function() {} // メソッド
Foo.B = function() {}           // クラスメソッド

Foo.prototype.A = function()
{
    this.B(); // メソッドが呼び出される
}

Foo.A = function()
{
    this.B(); // クラスメソッドが呼び出される
}

クラスプロパティ

コンストラクタのプロパティとして変数を設定すると、それがクラスプロパティとなります。

function Foo()
{
    this.x = 1; // プロパティ
}

// クラスプロパティ
Foo.x = 2;

var foo = new Foo();
var a = foo.x; // プロパティの参照: 1が返される
var b = Foo.x; // クラスプロパティの参照: 2が返される

継承

すべてのJavaScriptオブジェクトにはプロトタイプ オブジェクトというオブジェクトへの参照が含まれ、すべてのオブジェクトはこのプロトタイプからプロパティを継承しています。

// スーパークラス
function SuperClass( a, b )
{
    this.a = a;
    this.b = b;
}

SuperClass.prototype.Foo = function() {}
// サブクラス
function SubClass( a, b, c )
{
    // スーパークラスのコンストラクタを呼び出し、スーパークラスのプロパティを初期化する
    SubClass.call( this, a, b );

    // 新しいプロパティを追加する
    this.c = c;
}

// スーパークラスのサブクラスとなるように、プロトタイプオブジェクトを生成する
SubClass.prototype = new SuperClass();

// サブクラスのコンストラクタが参照されるように、既定のconstructorプロパティを書き換える
SubClass.prototype.constructor = SubClass;


// 新しいメソッドを追加する
SubClass.prototype.Bar = function() {}

// 継承したくないプロパティを、サブクラス側で削除する
delete SubClass.prototype.a;

参考

参考書

  • JavaScript 第5版 [オライリー・ジャパン] David Flanagan
    9章「クラスとコンストラクタとプロトタイプ」、10章「モジュールと名前空間」