原文:Classical Inheritance in JavaScript
※原文のコード内に誤りがあり、このまま記述しても動作しません。著者のコメント及び補足情報は翻訳メモを参照下さい。
君は自分が利口で階級にも属さず 自由だなんて思い込んでる
JavaScriptは、クラスという概念に囚われていない言語です。JavaScriptでは、古典的な継承の代わりにプロタイプ的な継承を使用します。これはC++やJavaのような旧来のオブジェクト指向言語に長けたプログラマを当惑させるかもしれません。JavaScriptのプロトタイプ的継承が、古典的継承に比べていかに表現力が優れているかを、これから見ていきましょう。
| Java | JavaScript |
|---|---|
| 強い型付け | 弱い型付け |
| 静的 | 動的 |
| 古典的 | プロトタイプ的 |
| クラス | 関数 |
| コンストラクタ | 関数 |
| メソッド | 関数 |
そもそも、私たちはなぜ継承を気にするのでしょうか?それには主に2つの理由があります。一つ目の理由は、型に関する利便性のためです。私達は言語システムに対して、類似するクラスの参照へ自動的にキャストしてもらうことを望みます。オブジェクト参照のキャストに、定型的で明示的な記述が必要な型システムからは型の安全性をほとんど得ることができないためです。これは強い型付け言語ではとても重要なのですが、JavaScriptのような弱い型付け言語ではオブジェクト参照はキャストの必要がないため大した意味を持ちません。
2つ目の理由は、コードの再利用です。沢山のオブジェクトの全てが、同一のメソッドを実装することは良くあります。クラスを使うことで、1セットの定義からそれら全てのオブジェクトを生成できるようになります。また、一部のメソッドだけが違っていたり多かったりする類似したオブジェクトが存在することもよくあります。この場面で、古典的継承も便利なのですが、プロトタイプ的継承はより便利です。
これを実証するために、まず旧来の古典的な言語に類似するパターンで書くためのシュガーを紹介し、続いて古典的な言語では利用できない有益なパターンを見ていきます。そして最後に、シュガーについて解説しようと思います。
まず最初に、Parenizorクラスを作りましょう。Parenizorクラスは、valueの設定および取得を行うメソッドと、valueを括弧で囲むtoStringメソッドを持ちます。
function Parenizor(value) {
this.setValue(value);
}
Parenizor.method('setValue', function (value) {
this.value = value;
return this;
});
Parenizor.method('getValue', function () {
return this.value;
});
Parenizor.method('toString', function () {
return '(' + this.getValue() + ')';
});
この構文はちょっと見慣れませんが、その中に容易に古典的なパターンを見て取ることができるでしょう。methodメソッドは、メソッド名と関数を取り、パブリックメソッドとしてそれらをクラスに追加します。
では、早速これを使ってみましょう。
myParenizor = new Parenizor(0); myString = myParenizor.toString();
期待通り、myStringは"(0)"になります。
次に、Parenizorを継承する別のクラスを作りましょう。
このクラスは、valueがゼロか空の場合にtoStringメソッドが"-0-"を返す以外は同じものです。
function ZParenizor(value) {
this.setValue(value);
}
ZParenizor.inherits(Parenizor);
ZParenizor.method('toString', function () {
if (this.getValue()) {
return this.uber('toString');
}
return "-0-";
});
inheritsメソッドは、Javaのextendsと似ています。uberメソッドは、Javaのsuperに類似し、メソッドの中からから親クラスのメソッドを呼び出します。(予約語による制約を回避するために、メソッドの名前を変えています。)
結果を確認しましょう。
myZParenizor = new ZParenizor(0); myString = myZParenizor.toString();
今度は、myString が"-0-"になりました。
JavaScriptにクラスはありませんが、まるでクラスがあるかのようにプログラムを書くこともできるのです。
関数のprototypeオブジェクトを操作することで、複数のクラスのメソッドから構成されたクラスを作り多重継承を実現できます。でたらめに多重継承を用いると実装が困難になり、メソッド名の衝突に苦しむ羽目になるかもしれません。JavaScriptでは、見境なく多重継承を使うこともできます。しかし次の例で、スイス継承というより規律ある方法を使ってみましょう。
ここではNumberValueというクラスを考えて見ましょう。NumberValueは、valueが一定範囲内の数字かをチェックし、必要に応じて例外を投げるsetValueメソッドを持ちます。私達がZParenizorに必要なのはsetValueとsetRangeメソッドだけで、toStringは全く要りません。では、これを書いてみます。
ZParenizor.swiss(NumberValue, 'setValue', 'setRange');
上記コードは必要なメソッドだけをクラスに追加しています。
ZParenizorを、別の方法で書くことができます。Parenizorを継承する代わりに、Parenizorのコンストラクタを呼び、身代わりとしてParenizorのインスタンスを返すコンストラクタを作成するのです。このコンストラクタでは、パブリックメソッドの代わりにプリビレッジド(特権)メソッドを追加します。
function ZParenizor2(value) {
var self = new Parenizor(value);
self.toString = function () {
if (this.getValue()) {
return this.uber('toString');
}
return "-0-"
};
return self;
}
古典的継承はis-a(これは-何々)関係です。対して寄生的継承は、was-a-but-now's-a(昔は-何々-だったが-今は-何々)関係といった感じです。オブジェクトの構築の場面で、コンストラクタが重要な役割を果たしています。依然としてuber(旧super)メソッドが、プリビレッジドメソッドの中から呼び出せる点にも注目してください。
JavaScriptのダイナミズムは、既存クラスに対するメソッドの追加や置換を可能にします。methodメソッドはいつでも呼び出すことができ、現存するインスタンスと未来のインスタンスの全てが、新たに追加されたメソッドを持つことになります。つまり、いつでも文字通りクラスを拡張(extend)できるということなのです。継承は過去に遡るように働きます。この拡張機構のことを、別のことを意味するJavaの拡張(extends)との混同を避けるため、ここではクラス補強(Class Augmentation)と呼びましょう。
静的なオブジェクト指向言語では、他のオブジェクトと少し異なるオブジェクトが必要な場合は新しいクラスを定義する必要がありました。しかしJavaScriptでは、クラスを追加しなくても個々のオブジェクトに直接メソッドを追加することができます。これはクラスの数を大きく減らし、よりシンプルにコードを書けるようにするため非常に強力です。JavaScriptのオブジェクトがハッシュテーブルに似ていることを思い出してください。いつでも新しい値をオブジェクトに追加できるのです。そして追加した値が関数ならば、それはメソッドになります。
先程の例では、ZParenizorクラスは全く必要ありませんでした。そして、簡単にインスタンスを修正することができました。
myParenizor = new Parenizor(0);
myParenizor.toString = function () {
if (this.getValue()) {
return this.uber('toString');
}
return "-0-";
};
myString = myParenizor.toString();
継承の形式を全く使わずに、myParenizorインスタンスにtoStringメソッドを追加しました。JavaScriptはクラスという概念に囚われていない言語なため、私たちは個々のインスタンスを発展させることができるのです。
これまでの例の中で、私は4つのシュガーメソッドを使いました。
最初は、methodメソッドです。これはクラスにインスタンスメソッドを追加します。
Function.prototype.method = function (name, func) {
this.prototype[name] = func;
return this;
};
ここではFunction.prototypeにパブリックメソッドを追加しています。このため全ての関数でクラス補強を行なえます。methodメソッドは名前と関数を引数に取り、関数のprototypeオブジェクトに追加します。
methodメソッドはthisを返します。私は通常、値を返す必要のないメソッドではthisを返すようにしています。これによりカスケードスタイル(縦列形式)のプログラミングが可能になるからです。
次は、inheritsメソッドです。これは、あるクラスが別のクラスを継承することを示します。inheritsメソッドは、親子両方のクラスを定義した後で、かつ、子クラスにメソッドが追加される前に呼び出す必要があります。
Function.method('inherits', function (parent) {
var d = 0, p = (this.prototype = new parent());
this.method('uber', function uber(name) {
var f, r, t = d, v = parent.prototype;
if (t) {
while (t) {
v = v.constructor.prototype;
t -= 1;
}
f = v[name];
} else {
f = p[name];
if (f == this[name]) {
f = v[name];
}
}
d += 1;
r = f.apply(this, Array.prototype.slice.apply(arguments, [1]));
d -= 1;
return r;
});
return this;
});
再びFunctionを補強します。parentクラスのインスタンスを作り、新しいprototypeとしてそれを使用します。constructorフィールドも修正し、同様にprototypeにuberメソッドを追加します。
uberメソッドは、自分自身のprototypeから該当する名前を持つメソッドを探します。見つかったメソッドは、寄生的継承かオブジェクト補強の場合に呼び出す関数となります。古典的継承を行なっている場合は、parentのprototypeの中から関数を探す必要があります。
returnステートメントは、Functionのapplyメソッドを使用して関数を呼び出します。明示的にthisをセットし、引数の配列を渡しています。引数(もしあれば)は、arguments配列から取得します。惜しいことにarguments配列は厳密には配列でないため、配列のsliceメソッドを呼び出すために、再びapplyを使用しなくてはいけないのです。
最後はswissメソッドです。
Function.method('swiss', function (parent) {
for (var i = 1; i < arguments.length; i += 1) {
var name = arguments[i];
this.prototype[name] = parent.prototype[name];
}
return this;
});
swissメソッドはargumentsをループし、parentのprototypeから新しいクラスのprototypeへ各nameに合致するメンバをコピーします。
古典的な言語のようにJavaScriptを使用することもできます。しかし、JavaScriptは比類なき高い表現力もまた備えているのです。私たちは古典的継承、スイス継承、寄生的継承、クラス補強、オブジェクト補強と見てきました。Javaより小さく簡単だと思われている言語から、これらの多様なコード再利用パターンのセットがもたらされるのです。
古典的なオブジェクトは固いです。固いオブジェクトに新しいメンバを加える唯一の方法は、新たなクラスを作ることです。一方JavaScriptのオブジェクトは柔らかいです。単純な代入で、柔らかなオブジェクトに新しいメンバを追加することができます。
非常に柔軟なJavaScriptのオブジェクトを知り、あなたはクラス階層について異なった考えを抱きたくなることでしょう。深い階層は、誤りです。浅い階層は、表現に富み効率的です。