JavaScript 2.0(ECMAScript第4版と事実上同じ)ではクラスが正式に採用されます。しかし、バージョン1.5までのJavaScriptにはクラスがありません。今回は、前回までに述べたことをもとに、クラスレスなJavaScriptにおいて、“クラスの継承”をシミュレートしてみます。
最初に、「クラスやクラス継承をシミュレートするやり方は一通りではない」ことを注意しておきます。ここで述べる方法は一例に過ぎません。ちなみに、JavaScript 2.0においてクラス概念を導入する理由のひとつは:「従来の機能を組み合わせて十分にクラスを実現できるが、そのやり方が色々あると、可搬性、相互運用性に問題が生じる」からだそうです -- だよなぁ。
今回の内容:
- いままでの復習と用語の約束
- 基底クラスの事例:Point
- 派生クラスの事例:ColoredPoint
- クラス継承を実現するための方針
- これがクラス継承のコーディングだ
- スーパー呼び出し
- 今回のまとめ
●いままでの復習と用語の約束
とても基本的で重要なこと:
- 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回を参照)の絵を描きましょう。色鉛筆使うといいですよ、三色ボールペンでもいいけど。
今回紹介した方法はまずまずうまく動きます。しかし、問題点がないわけでもなく、他の代替案も検討すべきです。で、それは次回にします。
- 現状のJavaScriptの機能を組み合わせて、クラスの継承をシミュレートできる。
- 継承実現のためには、サブクラスのインスタンスの__proto__チェーンに、スーパークラスのトレイツオブジェクトが含まれればよい。
- 継承のコーディングは、
Subclass.prototype.__proto__ = Superclass.prototype;
というパターンになる。 - スーパーコンストラクタの呼び出しは、
Superclass.apply(this, args);
とする。 - サブクラスのインスタンスからスーパークラスのメソッドにアクセスするには、
this.__proto__.__proto__.methodName
(ややトリッキー)、またはSuperclass.prototype.methodName
(こちらが無難)とする。
- ハブエントリー(全体目次)
- 次回 -