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

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

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

参照用 記事

プログラマのためのJavaScript (11):継承についてもう少し

前回(だいぶ前ですなー)、__proto__チェーンを操作することにより継承を実現する方法を紹介しました。しかし、この方法には問題があります。今回は、この問題点を指摘して代替案について説明しましょう。

今回の内容:

  1. __proto__を使った継承の問題点:これじゃ使えんぞ
  2. そうか、newを使えばいいのだ
  3. 親知らず? いや、おばさん子
  4. まだ問題がある
  5. さらに他にも問題がある
  6. 特定インスタンスはトレイツじゃないしぃ
  7. 今回のまとめ


●__proto__を使った継承の問題点:これじゃ使えんぞ

JavaScriptのクラス(もどき)は、「コンスタラクタ関数+トレイツオブジェクト」で実現できます。トレイオブジェクトとはコンストラクタ関数のprototypeオブジェクトであり、インスタンスの__proto__オブジェクトでした。

この前提で、クラスの継承は、Subclass.prototype.__proto__ = Superclass.prototype;というオマジナイを書けばOKです(その他にも、いくつか知っておいたほうがいいことがありましたけど)。

この方法は、__proto__プロパティの存在と操作可能性に頼ってます。しかし残念ながら、__proto__はECMAの標準ではなく、すべての環境で使えるものではありません。例えば、IEブラウザ環境ではダメです。

それじゃ使いものにならないって? そうかもしれません。困りました。

●そうか、newを使えばいいのだ

ここで、オブジェクト生成の仕掛け(第8回)を思い出してください。var foo = new Foo() で生成されたインスタンスfoo(正確には、変数fooが指すオブジェクト)は、自動的にFoo.prototypeを__proto__オブジェクトに持ちます。

この事実を利用すれば、Subclass.prototype.__proto__ = Superclass.prototype;の代わりに、Subclass.prototype = new Superclass(); としても、ほぼ同様な効果が得られます。図(この日記内[一時的注意]: まだ絵をアップロードしてません。自分で描いてみるのがいいオベンキョウかと。)を見れば事情を納得できるでしょう。

今度のオマジナイSubclass.prototype = new Superclass();には、__proto__が生で現れていないので、__proto__が使えない環境でも大丈夫です。めでたしめでたし -- でもないのですよね

●親知らず? いや、おばさん子

newで生成されたオブジェクトは単なるインスタンスで、本来のトレイツオブジェクトではありません。トレイツオブジェクトの特徴であるconstructorプロパティ(第8回を参照)を持ってません。するとどうなるか?次のようなことになります。


js> function Foo() {}
js> function Bar() {}
js> Bar.prototype = new Foo
[object Object]
js> var x = new Bar
js> x.constructor.name
Foo
js>

xは、まぎれもなくBarから生成されたにもかかわらず、xの親はFooだってことになってます。生みの親を間違えているのです。まー、FooはBarの姉のようなものですから、赤の他人ではないのが救いでしょうか。なんでこんなことになってしまうかは、第8回を読み直してもらえれば明らかになるはずです -- constructorプロパティの値は、__proto__チェーンをたどって求められることがキモです。

さて、対処としては、強引にconstructorプロパティを設定してやればいいでしょう。つまり、継承のためのコードを次のようにします。


Subclass.prototype = new Superclass();
Subclass.prototype.constructor = Subclass;

●まだ問題がある

これで済んだかというと、そうではありません。すべての環境でconstructorを設定できるわけではないのです。例えばRhinoでは、どうもconstructorを特別扱いしているらしく、設定できません。

constructorが正しく設定されてないと、instanceofも間違う可能性があります。なぜならinstanceofは、次の関数のようにして所属関係を判断するからです。


function isInstanceOf(obj, cls) {
for (var o = obj; o != null; o = o.__proto__) {
if (o.constructor == cls) return true;
}
return false;
}

ただしRhinoでは、constructorプロパティとは無関係にinstanceof演算子は正しい結果を返します。実際上は、constructorプロパティを直接使わずに、instanceof演算子を使うようにしほうが無難なようです。

●さらに他にも問題がある

クラスの静的メンバー(定数やクラスメソッド)にあたるものは、コンストラクタ関数オブジェクトのプロパティとして実現していました。前回の例だと、Point.MAX_X、Point.MAX_Y、Point.checkBoundsなどです。これら静的メンバーを派生クラスでも使えるようにするには、Subclass.__proto__ = Superclass;とすればいいのですが、ここでも__proto__が出てきてしまいます。静的メンバーの継承はあまり要らない気もしますが、必要なら次のような関数でプロパティをコピーするしかないでしょう。


function copyProperties(from, to) {
for (var p in from) {
if (typeof to[p] == 'undefined') {
to[p] = from[p];
}
}
}

スーパーコンストラクタ呼び出しは、Superclass.apply(this, args);スーパークラスのメソッド呼び出しは Superclass.prototype.methodName.apply(this, args); と、スーパークラスの名前を明示的に使えばいいでしょう。もし、どうしても名前を使わずにスーパークラスを指したいなら、次のようにでもしましょうか。


// superは予約語だから使えない
Subclass._superclass = Superclass;
Subclass._super = Superclass.prototype;

これらは次のように使えます。


/* スーパー呼び出し */

// コンストラクタ呼び出し
Thisclass._superclass.apply(this, args);
//クラスメソッド呼び出し
Thisclass._superclass.methodName.apply(this, args);
//インスタンスメソッド呼び出し
Thisclass._super.methodName.apply(this, args);

●特定インスタンスはトレイツじゃないしぃ

今まで述べたような対処法で、__proto__なしでも継承は実現できます。が、どうもまだ引っかかる点があるんですよ。Subclass.prototype = new Superclass();とすると、Subclassのトレイツオブジェクトとしてインスタンスが入るんですね。newを使うと(コンストラクタの)初期化処理が走るから、オブジェクトのインスタンス変数がセットされたりするのです。そんな特定インスタンスの個別的性格がサブクラスのインスタンス全体に伝搬してしまうのはどうかな? と思うのです。

それと、new Superclass() と引数なしのコンストラクタを使いましたが、コンストラクタに引数必須だったら、いったいどんな値を入れりゃいいんでしょう。やっぱり、特定個別のインスタンスをトレイツに流用するのは変な感じがします。

と言っても、まーらちがあかないから、今回の流儀で書いた継承のコード例を挙げましょう。前回と同じくColoredPointです。せっかくだから(?)、_superclass、_superを使っています。


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

/* このクラスのコンストラクタ */
function ColoredPoint(color_, x_, y_) {
if (typeof color_ == 'undefined') color_ = ColoredPoint.BLACK;
// superconstructor call
ColoredPoint._superclass.apply(this, [x_, y_]);
this.color = color_;
}
/* 継承 */
ColoredPoint.prototype = new Point();
copyProperties(Point, ColoredPoint); // 必要なら
ColoredPoint._superclass = Point; // お好みにより
ColoredPoint._super = Point.prototype;// お好みにより

/* メソッドのオーライド */
ColoredPoint.prototype.toString = function() {
// super method call
var s = ColoredPoint._supper.toString.apply(this, []);
return s + "/" + this.color;
}

●今回のまとめ

  1. __proto__は、どの環境でも使えるわけではない。
  2. したがって、__proto__を操作して継承を実現する方法に一般性はない。
  3. newを使った Subclass.prototype = new Superclass(); が継承の実現に使える。
  4. newを使ったときは、constructorの設定をしなくてはならない。が、すべての環境でconstructorが設定できるわけではない。
  5. 静的メンバの継承はプロパティのコピーを使う。
  6. 特定インスタンスを、トレイツオブジェクトとするのは自然とは言えない。と、檜山は思うが、ここは妥協するしかあるまい。

次回はスコーピングを話題にするつもりです。