The Dabsong Conshirtoe

技術系の話を主にします。

Backbon.jsのextendの仕組み

Backbone.js Advent Calendarの13日目を担当しますAttsun_1031です。普段はpythonとjs使ってます。

Backbone.js使いでもないんですが、javascriptでコード再利用のパターンを検討している時にBackbone.jsのextendがヒッジョーに参考になったので、その仕組みについて書きます。(バージョンは0.9.2)
どうでも良い話ですが、python(ニシキヘビ)って背骨あるんでしょうか。

extendって?

新しいクラスを定義するために使います。extendの引数に渡したオブジェクトがそのクラスのインスタンスのメンバーになります。第2引数に渡したオブジェクトはクラスの静的なメンバーになります。利用例は以下。

// Personクラスを定義する
var Person =  Backbone.Model.extend({
  // initializeはBackbone.jsで規定されている初期化処理で、newした時に呼ばれます。
  initialize: function(name, age) {
    this.name = name;
    this.age = age;
  },

  say: function() {
    return "I am " + this.name;
  }
}, {
  TYPE: "Mammal"
});

// Personのインスタンスを生成
var me = new Person('Attsun_1031', 26);
console.log(me.say());
> 'I am Attsun_1031'
console.log(Person.TYPE);
> 'Mammal'

本来、プロトタイプベースのオブジェクト指向言語であるjsにはクラスやインスタンスといった概念が存在しないのですが、Backbone.jsではそれをエミュレートすることでコード再利用の方法を提供しています。
もちろん、こんな仕組みなくてもコード再利用は可能なんですが、クラスベースOOPに慣れてる人が圧倒的に多いからこうしてるんでしょうかね。
私もJavaPythonのようなクラスベースのOOPを触っているのでクラス・インスタンスという概念があるほうが書きやすいです。

extendの仕組み

では本題に戻ってextendのソースコードを見ながら仕組みを紐解いていきます。

  var extend = function (protoProps, classProps) {
    var child = inherits(this, protoProps, classProps);
    child.extend = this.extend;
    return child;
  };

これがextendのコードです。
protoPropsというのがインスタンスメンバー、classPropsというのがその名の通りクラスの静的なメンバーです。
動作は、inherits関数を呼んでchildを生成し、そのchildにextendメソッドを設定して返します。
これは、サブクラスから更なる継承を可能にするためです。

終わり。

・・・

ではなくて、肝心のinheritsメソッドの中身を見てみましょう。もとのコードからコメントを削除し、番号を振ってます。

  var ctor = function(){};

  var inherits = function(parent, protoProps, staticProps) {
    // 1
    var child;

    // 2
    if (protoProps && protoProps.hasOwnProperty('constructor')) {
      child = protoProps.constructor;
    } else {
      child = function(){ parent.apply(this, arguments); };
    }

    // 3
    _.extend(child, parent);

    // 4
    ctor.prototype = parent.prototype;
    child.prototype = new ctor();

    // 5
    if (protoProps) _.extend(child.prototype, protoProps);
    if (staticProps) _.extend(child, staticProps);

    // 6
    child.prototype.constructor = child;

    // 7
    child.__super__ = parent.prototype;

    return child;
  };

1. 戻り値となるchildを定義します。これがサブクラス(コンストラクタ関数)です。こういう長い関数において、冒頭に戻り値を定義するのは良いプラクティスですね。


2. protoPropsに 'constructor' という名前の関数がある場合、それをchildにセットします。そうでない場合は、child自身をthisとしてparent関数を呼ぶ関数をセットします。parent関数はextendを呼んでいる関数なので、冒頭の例でいえばModel関数がコールされたことになります。
Model関数の詳細は省きますが、内部でthis.initialize.applyをコールしています。つまり、constuructorの指定がなければinitializeが必ず呼ばれる、ということになります。


3. これはBackbone.jsが依存しているunderscore.jsのextendを呼んでいる(と思われます)。childにparentのプロパティ(メソッドとか)をコピーしているのです。


4. inheritsの上で定義しているctorという空の関数にparentのprototypeをセットし、childのprototypeにはそれをnewしたものをセットします。
単純にparentのインスタンスをprototypeとしてセットしたいだけなら

child.prototype = new parent();

とやってしまえば良いのですが、これだとparentのインスタンス固有の変数(parent関数内でセットされる値)をchildが受け継いでしまいます。
やりたいのは、parentのプロトタイププロパティのみ継承することなので、空の関数にprototypeをコピーし、それをnewすることで解決しています。

この辺の話を理解するにはプロトタイプチェーンの理解が欠かせません(私も最近学びました)。オライリーの「JavaScriptパターン ―優れたアプリケーションのための作法」が非常に参考になります。この一時的なコンストラクタのテクニックの他、コード再利用パターンが色々書いてあるのでオススメです。


5. child.prototypeにprotoPropsをコピーします。これが所謂インスタンスメンバーになります。また、childそのものにstaticPropsをコピーすることで、Child.HOGEHOGEのような呼び出しができる静的メンバーをセットします。


6. child.prototypeのconstructorにchild自身をセットします。このconstuructorは2で出てきたconstuructorとは別物で、javascriptのあらゆるオブジェクトが持つプロパティです。これをしないとconstructorがparentのままになってしまうので設定しています。


7. __super__にparent.prototypeをセットします。こうすることで子クラスの中で以下のように親のメソッドを呼ぶことが可能になります。

Child.__super__.parentMethod.apply(this, some_args);


以上です。
extendはクラスベースOOPにおける継承をエミュレートしていますが内部ではプロトタイプの仕組みを使っているので、非常に勉強になりました。

JavaScriptパターン ―優れたアプリケーションのための作法

JavaScriptパターン ―優れたアプリケーションのための作法