JavaScript におけるクラスの作成と継承

JavaScript はプロトタイプベースのオブジェクト指向言語であり、クラスという概念を持っていない *1、というのはよく言われることですが、new 演算子とコンストラクタとプロトタイプを使うことでクラスのようなものを作ってインスタンス化することが可能です。 さらに、プロトタイプを使ってクラスの継承も実現できます。

この記事では、JavaScript においてクラスベースの言語と似たようなことをする方法について説明します。

用語

まずは用語の説明をします。 ECMAScript (5th edition) *2 に書かれているものを訳しました。

コンストラクタ (constructor)
オブジェクトを生成し、初期化する関数オブジェクト
プロトタイプ (prototype)
別のオブジェクトに共有されたプロパティを提供するオブジェクト
プロパティ (property)
オブジェクトの一部で、名前と値が関連付けられたもの。 オブジェクト obj に対してプロパティ name にアクセスするには obj.name とする
自身のプロパティ (own property)
あるオブジェクト自身に直接含まれているプロパティ
継承したプロパティ (inherited property)
あるオブジェクト自身のプロパティではないが、そのオブジェクトのプロトタイプに含まれているプロパティ

オブジェクトは内部にプロトタイプへの参照を 1 つ持っています。 オブジェクトのプロパティが参照されたとき、そのオブジェクト自身がそのプロパティをもっていない場合に、プロトタイプのプロパティが調べられ、もしプロトタイプがそのプロパティを持っていればそれが返されます。 この仕組みを利用して、クラスやクラスの継承といったものの真似事ができます。

方針

以下のような方針でクラスベースのオブジェクト指向の真似事をします。

  • オブジェクトにプライベートな領域 (オブジェクト外部からアクセスできない変数やメソッド) は持たせない
  • いわゆるインスタンス変数はオブジェクト自身のプロパティとして設定する
  • いわゆるインスタンスメソッドは継承したプロパティとして設定する

クラスの定義とインスタンス

まずは継承を使わず、単一のクラスを作ってみます。 new 演算子をコンストラクタに作用させると、新しいオブジェクトが作られて、コンストラクタが実行されます。 このとき、コンストラクタ内の this は新しく作られたオブジェクトを参照します。

とりあえず、コンストラクタとして適当に関数を作ってみます。 コンストラクタ内の this のプロパティとして値を設定すると、それが新しいオブジェクト自身のプロパティとなります。 方針のところで述べたように、インスタンス変数はオブジェクト自身のプロパティとして設定したいので、コンストラクタ内でインスタンス変数を設定します。

// コンストラクタを定義
//   コンストラクタの名前はクラス名にしておくと良い. 
//   クラス名の最初の文字を大文字にすることでクラスであることをわかりやすくする
var Class = function( var1 ) {
    // インスタンス変数の設定
    this.var1 = var1;
    // コンストラクタ関数は戻り値を明示しなくて良い
};

// インスタンス化してみる
//   new 演算子をコンストラクタに作用させることでインスタンス化する
var c1 = new Class( 100 );
// インスタンス変数がちゃんと設定されている
alert( c1.var1 ); // "100"

次に、プロトタイプにメソッドの追加を行います。 new 演算子をコンストラクタに作用させたとき、コンストラクタの prototype プロパティの参照先が新しく作られるオブジェクトのプロトタイプとなります。 つまり、メソッドはコンストラクタの prototype プロパティに追加します。

// 上の続き

// メソッドを定義
Class.prototype.view = function() {
    // this は新しく生成されるオブジェクト (インスタンス) を参照する
    alert( "Class クラスのインスタンス [var1 : " + this.var1 );
};

// メソッドを実行してみる
//   インスタンス化した後に追加したメソッドも実行できる
c1.view();

まとめると次のようになります。

// コンストラクタを定義する
var Class = function( args ) {
    // コンストラクタ関数内でインスタンス変数の設定
    /* ... */
};
// コンストラクタの prototype プロパティのプロパティとして
// インスタンスメソッドを定義する
Class.prototype.methodName1 = function( args ) { /* ... */ };
Class.prototype.methodName2 = function( args ) { /* ... */ };

クラスの継承

次にクラスの継承について説明します。

クラスの継承を実現するために、以下のことを行う必要があります。

インスタンス変数の初期化は、派生クラスのコンストラクタから親クラスのコンストラクタを明示的に呼び出すことで実現します。 メソッドの継承は、親クラスのコンストラクタの prototype プロパティの参照先オブジェクトをプロトタイプとして持つオブジェクトを派生クラスのコンストラクタの prototype プロパティに設定することで実現します。 そのためのメソッドを Function.prototype に追加します。

// 継承のためのメソッド
// baseConstructor は親クラスのコンストラクタ
Function.prototype.extend = function( baseConstructor ) {
    // 親クラスのコンストラクタの prototype プロパティの参照先オブジェクトを
    // プロトタイプとして持つオブジェクトを作る
    var F = function(){};
    F.prototype = baseConstructor.prototype;
    // 派生クラスのコンストラクタの prototype プロパティに設定
    this.prototype = new F();
      // ここでは互換性を考えて上の方法をとっているが
      // Object.create が使えるなら
      //     this.prototype = Object.create( baseConstructor.prototype );
      // でよい

    // コンストラクタを示すプロパティを設定
    this.prototype.constructor = this;
    this.baseConstructor = baseConstructor;
    return this;
};

あとは上で定義した Function.prototype.extend メソッドを使うだけです。 注意点としては、派生クラスから親クラスのコンストラクタを呼び出すことを忘れないようにしなければいけない、ということです。 Function.prototype.extend メソッドを使うことで、親クラスのコンストラクタは派生クラスのコンストラクタの baseConstructor プロパティで参照できるので、SubClass.baseConstructor で取得しています。 さらに、親のコンストラクタ内で thisインスタンスを指すように、Function.prototype.apply メソッドを使用しています。

// 親クラスの定義
var BaseClass = function BaseClass( args ) {
    /* ... */
};
BaseClass.prototype.method = function() { /* ... */ };

// 派生クラスの定義
var SubClass = function SubClass( args, args2 ) {
    // 親クラスのコンストラクタを明示的に呼び出す (引数は適当に指定)
    SubClass.baseConstructor.apply( this, [args] );
    // インスタンス変数の定義など
    /* ... */
}.extend( BaseClass ); // BaseClass を継承
SubClass.prototype.method = function() { /* ... */ };

これで継承もできるようになりました。

JavaScript でクラスベースの書き方をする意義について

とまあ長々と説明してきましたが、JavaScript であえてクラスベースの書き方をする意義があるのか、というとあまりないような気はします。 もちろん、同じメソッドを持っていて値だけが異なるオブジェクトを大量に作りたい、というようなときにはクラスベースでの書き方をするといいと思います。

しかし、他のクラスベースの言語 (Java とか Ruby とか) に慣れているから、という理由だけで JavaScript でもクラスベースの書き方をするのはやめておいた方がいいでしょう。 JavaScript では、他の方法を使って簡単にオブジェクトの生成ができます。 なんでもかんでもクラスベースの書き方にする、というのではなく、必要に応じてクラスベースの書き方を使ってください。

*1:ECMAScript には Class という言葉が出てきますが、ここでいうクラスとはまた別です。 ここでは、ユーザーが定義可能な型としてのクラスのことを言っています。

*2:JavaScript やそれに類する言語の統一仕様