ES5なJavascriptでモダンなクラス的継承&スーパー呼び出し

  • 52
    いいね
  • 1
    コメント
この記事は最終更新日から1年以上が経過しています。

動機

AltJSな言語ではなく、素のES5なJavascriptでクラスベース的継承ってどう書くのか、親クラスのメソッドの呼び出し方はどうか、モダンな書き方ってどうなのか、あらためて勉強してみた。

(この投稿ではES6やプロトタイパルな継承はあつかいません)

継承

[JavaScript] そんな継承はイヤだ - クラス定義 - オブジェクト作成

↑の投稿が参考になりました。

結論としては、util.inherits(ブラウザバージョン)のやり方がモダンな正しいクラス的継承。

function inherits(ctor, superCtor) {
  ctor.super_ = superCtor;
  ctor.prototype = Object.create(superCtor.prototype, {
    constructor: {
      value: ctor,
      enumerable: false,
      writable: true,
      configurable: true
    }
  });
};
function Parent(){}
function Child(){}

inherits(Child, Parent);

var c = new Child();

継承についてはほぼこれだけが、たったひとつの冴えたやり方といえるが、親クラスのメソッドの呼び出し方には、いろいろな流派があるようだ。

親クラスのメソッドの呼び出し

基本

function Child(arg){
    Parent.call(this, arg);
}

Child.prototype.methodName = function(arg) {
    Parent.prototype.methodName.call(this, arg);
};

オーソドックスな呼び出し方だが、親クラスを明示的に書かなければならない。

少し改良

上記inherits関数は、親クラスのコンストラクタを子クラスのコンストラクタのsuper_プロパティに保持するため以下のように書ける。

function Child(arg){
    Child.super_.call(this, arg);
}

Child.prototype.methodName = function(arg) {
    Child.super_.prototype.methodName.call(this, arg);
};

明示的な親クラスの名前がソースコードから消えているのがわかる。
親コンストラクタの呼び出しは簡潔だが、ほかのメソッドからの親メソッドの呼び出しが長い。
ていうか、コンストラクタとそれ以外のメソッドで呼び出し方が違うのが気になる。

呼び出し方をそろえる

inherits関数で

  ctor.super_ = superCtor;

↑こうなっている部分を仮に

  ctor._super_ = superCtor.prototype;

と書き換えると以下のように書ける。

function Child(arg){
    Child._super_.constructor.call(this, arg);
}

Child.prototype.methodName = function(arg) {
    Child._super_.methodName.call(this, arg);
};

呼び出し方は揃ったけど、あいかわらず長い。
また自分自身のクラスの名前を依然として明示的に書く必要がありモヤモヤする。

thisに保持させる(ダメな例)

function inherits(ctor, superCtor) {
  ctor.prototype = Object.create(superCtor.prototype, {
    constructor: {
      value: ctor,
      enumerable: false,
      writable: true,
      configurable: true
    },
    _super_: {
      value: superCtor.prototype,
      enumerable: false,
      writable: true,
      configurable: true
    }
  });
};

こうしとけば、

function Child(arg){
    this._super_.constructor.call(this, arg);
}

Child.prototype.methodName = function(arg) {
    this._super_.methodName.call(this, arg);
};

こう書けるんじゃね?
と一見思えなくもないが、

function GrandChild(arg){
    this._super_.constructor.call(this, arg);
}

GrandChild.prototype.methodName = function(arg) {
    this._super_.methodName.call(this, arg);
};

継承が3階層以上になると2階層目のsuper呼び出しで無限ループで死ぬ。
GrandChildのインスタンスのthis._super_が指しているのは常にChild.prototypeなのだから当然だ。

と、ここまでが前フリです。

歴史を振り返る

Douglas Crockfordの方法

http://javascript.crockford.com/inheritance.html

Child.prototype.methodName = function(arg) {
    this.uber("methodName", arg);
};

通常のメソッド内では上のように書けるが、コンストラクタ内では同じやり方で書けない。
initメソッドなどの初期化メソッドを用意してその中でuberメソッドを呼ぶ。

uberはメソッドの呼び出された階層の深さを記録して、その分だけprototypeチェーンをたぐっている。

prototype.jsの方法

http://prototypejs.org/learn/class-inheritance.html

var Child = Class.create(Parent, {
    mathodName: function($super, arg) {
        $super(arg);
    }
});

メソッドの仮引数の先頭に$superがあるとそれを検知して、親クラスの同名メソッドを呼び出す関数を第一引数に渡す関数でメソッドをwrapするという黒魔術。
何を言ってるのかわからねーと思うが(略

Josh Gertzenの方法

http://joshgertzen.com/object-oriented-super-class-method-calling-with-javascript/

var Child = Parent.extend({
    mathodName: function(arg) {
        arguments.callee.$.methodName.call(this, arg);
    }
});

メソッド自体に属性として親クラスのprototypeを持たせてそれをarguments.calleeで参照。
だがarguments.callee自体が非推奨であるため正直微妙。

Dean Edwardsの方法

http://dean.edwards.name/weblog/2006/03/base/
http://dean.edwards.name/weblog/2007/12/base2-intro/

var Child = Parent.extend({
    mathodName: function(arg) {
        this.base(arg);
    }
});

this.baseに格納した親クラスをメソッド呼び出し前に差し替え、呼び出し後に元に戻すというクロージャでメソッドをwrapするという黒魔術。
何を言ってるのかわからねーと思うが(略

John Resigの方法

http://ejohn.org/blog/simple-javascript-inheritance/

var Child = Parent.extend({
    mathodName: function(arg) {
        this._super(arg);
    }
});

メソッドの定義の中に_super呼び出しが存在すれば、this._superに格納した親クラスをメソッド呼び出し前に差し替え、呼び出し後に元に戻すというクロージャでメソッドをwrapするという黒魔術。
何を言ってるのかわからねーと思うが、prototype.jsとBase2のアイデアをミックスしたものらしい。

Dean EdwardsとJohn Resigの方法は、親クラスを保持するインスタンス変数を親クラスのメソッド実行時に差し替えるという共通のトリックを用いている。
しかし、親メソッド実行途中に例外が発生した場合には、親クラスを元に戻す過程が実行されなくなってしまう。

Fiber.jsの方法

https://github.com/linkedin/Fiber

var Child = Parent.extend(function(base) {
    return {
        mathodName: function(arg) {
            base.methodName.call(this, arg);
        }
    };
});

親クラスをレキシカル変数に保持する方法。
記述は多少冗長になるが、リフレクションもどきやインスタンス変数の動的差し替えなどの黒魔術を使う必要がない。

ところでこの方法、初出はどなたなんでしょうか?
ご存じの方はお知らせください。
日本では右京webさんが2012/6に、ほぼ同様のアイデアを投稿されているようです。
http://hujimi.seesaa.net/article/278050880.html

結局どれを使えば良いのか?

以上、親クラスのメソッド呼び出しの主な手法をリストアップしてみました。
どのパターンを利用するかは、それぞれの得失を考慮した上での好みかな、と。

(記述のコンパクトさ重視なら、John Resigスタイルで、とか。)

ところで

今回、勉強しなおした成果(?)を、オレオレ継承ライブラリにしてみました。
Fiberスタイルですが名前空間を汚さないという個人的方針の為、クラスや継承の記述はもっとダサい感じに仕上がっています。

https://github.com/no22/qoo

そんじゃーね!