このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

プログラマのためのJavaScript (10):クラスもどきの継承もどき

継承の仕組み


JavaScript 2.0ECMAScript第4版と事実上同じ)ではクラスが正式に採用されます。しかし、バージョン1.5までのJavaScriptにはクラスがありません。今回は、前回までに述べたことをもとに、クラスレスなJavaScriptにおいて、“クラスの継承”をシミュレートしてみます。

最初に、「クラスやクラス継承をシミュレートするやり方は一通りではない」ことを注意しておきます。ここで述べる方法は一例に過ぎません。ちなみに、JavaScript 2.0においてクラス概念を導入する理由のひとつは:「従来の機能を組み合わせて十分にクラスを実現できるが、そのやり方が色々あると、可搬性、相互運用性に問題が生じる」からだそうです -- だよなぁ。

今回の内容:

  1. いままでの復習と用語の約束
  2. 基底クラスの事例:Point
  3. 派生クラスの事例:ColoredPoint
  4. クラス継承を実現するための方針
  5. これがクラス継承のコーディングだ
  6. スーパー呼び出し
  7. 今回のまとめ

●いままでの復習と用語の約束

とても基本的で重要なこと:

  • JavaScriptが扱う複合データは、名前/値ペア(プロパティ)の集まりである。これを“オブジェクト”と呼ぶ。
  • 関数も特別な種類のオブジェクトである。
  • 値が関数(への参照)であるプロパティをメソッドと呼ぶ。

__proto__とprototypeについて、次のことを再確認してください。

  • プロパティの実行時検索は、__proto__チェーンをたどって実行される。メソッド検索も、もちろん同様。
  • 関数がコンストラクタとして使われたとき、その関数のprototypeオブジェクトが、生成されたオブジェクトの__proto__プロパティにセットされる。

以下では、コンストラクタ関数とトレイツオブジェクト(コンストラクタ関数のprototypeオブジェクト)の組をクラスと呼び、当該のコンストラクタで生成されたオブジェクトを(クラスの)インスタンスと呼ぶことにします。

あ、言い忘れた。僕は、「プロトタイプオブジェクト」という言葉を嫌って、代わりに「トレイツオブジェクト」(traits object)を使ってます。トレイツオブジェクトは、コンストラクタのprototypオブジェクトであり、同時にインスタンスの__ proto__オブジェクトで、デフォルト値やメソッドを集中的に定義する場所です。

以上は、単にローカルな用語の約束に過ぎません。「クラスとはそもそも……」といった、どうでもいいツマラナイ議論の対象にはしないでくださいね。

●基底クラスの事例:Point

次のPointクラスを事例にします。(ほとんど常にこの例だなー、僕は。)


/* クラスの定数 */
Point.MAX_X = 1000;
Point.MAX_Y = 1200;

/* クラス(静的)メソッド */
Point.checkBounds = function(x, y) {
if (Math.abs(x) > Point.MAX_X) {
throw new Error("out of bounds - x");
}
if (Math.abs(y) > Point.MAX_Y) {
throw new Error("out of bounds - y");
}
}

/* コンストラクタ */
function Point(x_, y_) {
if (typeof x_ == 'undefined') x_ = 0;
if (typeof y_ == 'undefined') y_ = 0;
Point.checkBounds(x_, y_);
/* インスタンス初期化コード */
this.x = x_;
this.y = y_;
}

/* インスタンスメソッド */
Point.prototype.moveTo = function(newX, newY) {
Point.checkBounds(newX, newY);
this.x = newX;
this.y = newY;
};
/* インスタンスメソッド(オーバライド) */
Point.prototype.toString = function() {
return "(" + this.x + ", " + this.y + ")";
};

このクラスの使用例はこんな感じ:


js> var p = new Point()
js> p
(0, 0)
js> var q = new Point(100, 200)
js> q
(100, 200)
js> p.moveTo(-200, 500)
js> p
(-200, 500)
js> q.moveTo(0, 2000)
js: "<stdin>", line 389: exception from uncaught JavaScript throw: [object Error]
js> try {q.moveTo(0, 2000);} catch(e) {print(e);}
Error: out of bounds - y
js>

●派生クラスの事例:ColoredPoint

Pointクラスを拡張(extends)してColoredPointを定義したいと思います。ColordPointはその名の通り「色付きの点」で、色は整数値で表現されるとしましょう。このことを、JavaScript 2.0風の構文で書けば、次のようになるでしょう。雰囲気が分かれば、詳細を気にする必要はありません(僕もJavaScript 2.0の仕様をちゃんと知ってるわけじゃないし)。


class ColoredPoint extends Point {
/* 追加の定数 */
static final var BLACK = 0;
... // 色の名前が並ぶ

/* 追加のインスタンス変数 */
var color;

/* このクラスのコンストラクタ */
function ColoredPoint(color_, x_, y_) {
super(x_, y_);
if (typeof color_ == 'undefined') color_ = BLACK;
color = color_;
}

/* メソッドのオーバライド */
function toString() {
var s = super.toString();
return s + "/" + this.color;
}
}

上記のような、Pointを継承したサブクラスColoredPointを、現状のJavaScriptの機能内でヤリクリして実現します。これがもしうまくできれば、次のようなことになるはずです。


js> var p = new ColoredPoint()
js> p
(0, 0)/0
js> p.moveTo(100, 200)
js> p
(100, 200)/0
js> p.color = 123
123
js> p
(100, 200)/123
js> p instanceof ColoredPoint
true
js> p instanceof Point
true
js> ColoredPoint.BLACK
0
js> ColoredPoint.MAX_X
1000
js>

●クラス継承を実現するための方針

さーて、ここから先は腰を据えて、ユックリ、ジックリ、ヨーク考える必要があります。

派生クラス(サブクラス)であるColoredPointのインスタンスから、基底クラス(スーパークラス)であるPointクラスのインスタンスメソッドもすべて使えなくてはなりません。

これはつまり、ColoredPointインスタンスのメソッド検索(プロパティ検索)の際に、Pointクラスのトレイツオブジェクトが検索対象になればいいのです。言い換えれば、インスタンスの__proto__チェーン(検索パス)にPointクラスのトレイツオブジェクトが含まれればいいのですね。

ここで図(今日の日記内にある)を見てください。いつものように、四角がコンストラクタ(P=Point, CP=ColoredPoint)、丸がそれ以外のオブジェクトです。インスタンスが赤で、トレイツがピンクですが区別できますか? 水平方向の両矢印は prototype/constructor による関連、上方向の矢印は__proto__チェーンの一部を表現します。

この図のような状況を作れば、望んでいたこと、つまり「ColoredPointインスタンスの__proto__チェーンに、Pointクラスのトレイツオブジェクトが含まれる」ことになります。

●これがクラス継承のコーディングだ

次のようなコードで、ColoredPoint extends Point というクラス(のあいだの)継承(派生、拡張、サブクラス定義)が実現できます。

スーパーコンストラクタ、スーパークラスのメソッドを呼び出すために使われているapplyが分かりにくいと思うので、次節で説明します。他の部分は、先の図とコード内コメントを参考にすれば理解できるでしょう。


/* 追加の定数 */
ColoredPoint.BLACK = 0;
// ... 色の名前が並ぶ

/* このクラスのコンストラクタ */
function ColoredPoint(color_, x_, y_) {
// スーパーコンストラクタを呼び出す
Point.apply(this, [x_, y_]);
if (typeof color_ == 'undefined') color_ = ColoredPoint.BLACK;
this.color = color_;
}
/* 継承 */
ColoredPoint.prototype.__proto__ = Point.prototype;
ColoredPoint.__proto__ = Point; // 場合により不要

/* メソッドのオーバライド */
ColoredPoint.prototype.toString = function() {
// スーパークラスのメソッドを呼び出す
var s = (this.__proto__.__proto__.toString).apply(this, []);
return s + "/" + this.color;
};

●スーパー呼び出し

funが関数、objがオブジェクト、argsが[arg1, arg2, ...]という配列だとして、fun.apply(obj, args);は次のコードと同じ効果があります。


obj.meth = fun;
obj.meth(arg1, arg2, ...);

このapplyを使って、スーパークラスのコンストラクタ/メソッド呼び出しを行います。

例えば、ColoredPointのコンストラクタ内で、スーパークラスであるPointのコンストラクタ(スーパーコンストラクタ)を呼び出す必要があります。もちろん、JavaScriptが自動でやってはくれないので、常に明示的呼び出しが必要なのです。ここでは、Point.apply(this, [x_, y_]); として呼び出しています。これは、次と同じです。


this.superConstructor = Point;
this.superConstructor(x_, y_);

単に Point(x_, y_); として呼び出すと、Point内で使うthisとして大域オブジェクトが渡されてしまいます(結果は大域オブジェクトを汚すだけ)。

また、ColoredPointのtoString定義内で、PointのtoStringを呼び出すところでは、(this.__proto__.__proto__.toString).apply(this, []);を使ってます。これは、(Point.prototype.toString).apply(this,[]);と書いたほうがわかりやすい(そしてベター)でしょうが、あえてえぐい書き方をしてみました。

ColoredPointのインスタンスから見て、tihs.__proto__はColoredPointクラスのトレイツオブジェクトです。そして、tihs.__proto__.__proto__はPointクラスのトレイツオブジェクトであり、PointクラスのtoStringはそこに定義されているのです(図を参照)。次のように分解して書くと分かりやすいでしょう。


// JavaScript 2.0なら、s = super.toString();
var s;
var superTraits = tihs.__proto__.__proto__;
this.superMethod = superTraits.toString;
s = this.superMethod();

●今回のまとめ

わかりにくいと思ったら、とにかくオブジェクトグラフ(シリーズ第4回を参照)の絵を描きましょう。色鉛筆使うといいですよ、三色ボールペンでもいいけど。

今回紹介した方法はまずまずうまく動きます。しかし、問題点がないわけでもなく、他の代替案も検討すべきです。で、それは次回にします。

  1. 現状のJavaScriptの機能を組み合わせて、クラスの継承をシミュレートできる。
  2. 継承実現のためには、サブクラスのインスタンスの__proto__チェーンに、スーパークラスのトレイツオブジェクトが含まれればよい。
  3. 継承のコーディングは、Subclass.prototype.__proto__ = Superclass.prototype; というパターンになる。
  4. スーパーコンストラクタの呼び出しは、Superclass.apply(this, args); とする。
  5. サブクラスのインスタンスからスーパークラスのメソッドにアクセスするには、this.__proto__.__proto__.methodName(ややトリッキー)、またはSuperclass.prototype.methodName(こちらが無難)とする。