🧊

_.extend() は いわゆる"継承"ではない

どうも、JavaScriptゆとり世代です。
JavaScriptで継承ってキーワード聞いて、どんなコードを連想しますか?

なんと私が最初に連想したのは、UnderscoreとかBackboneとかのextendでした・・。

※注:うまくまとまる気のしないメモ記事です。

結論からいうと

勘違いというか、そもそも完全に己が無知を晒すことになるんですけど、

いわゆる"継承"で実現したいであろう機能のひとつである、

  • 親クラスAがあって、それを継承して子クラスBを作った場合、
  • 子クラスBにも、親クラスAにも同名のメソッドFが定義されてても、
  • 子クラスBは親クラスAを継承してるので、親のメソッドFを実行することができる

恥ずかしながら、この"子から親の同名のメソッドを実行する"って発想がまるっとなかったんですよね。
むしろそういうことできるから、"継承"って言うんや!っていう。

_.extend()

コードを抜粋。

// Extend a given object with all the properties in passed-in object(s).
_.extend = function(obj) {
  if (!_.isObject(obj)) return obj;
  _.each(slice.call(arguments, 1), function(source) {
    for (var prop in source) {
      obj[prop] = source[prop];
    }
  });
  return obj;
};

コード見れば一目瞭然なんですけど、いわゆる"継承"ではないんですよね。
ただ単に、プロパティを代入してるだけ。

_.extend(Model.prototype, Events, {
  // ...
};

みたく、prototypeに突っ込めばいわゆる継承っぽいこともできるので、一概に間違ってはいないんやろうけど。
それでもプロパティ名が被ったら、上書きされちゃうし、コピーではないし。

いわゆる継承って何と言われればまた定義が必要になりますが、
なんでこんな風に認識してたのかなーと思うと、単に知らんかったとしか言えず。
単純にプロトタイプをどんどん生やしていくのが"継承"ではないということで。

改めて精進せにゃなーという次第。
JavaScriptゆとり世代な人は要注意?です。

Backbone.**.extend()

せっかくなので、Backboneのもみてみる。

function (protoProps, staticProps) {
  var parent = this;
  var child;

  // The constructor function for the new subclass is either defined by you
  // (the "constructor" property in your `extend` definition), or defaulted
  // by us to simply call the parent's constructor.
  if (protoProps && _.has(protoProps, 'constructor')) {
    child = protoProps.constructor;
  } else {
    child = function(){ return parent.apply(this, arguments); };
  }

  // Add static properties to the constructor function, if supplied.
  _.extend(child, parent, staticProps);

  // Set the prototype chain to inherit from `parent`, without calling
  // `parent`'s constructor function.
  var Surrogate = function(){ this.constructor = child; };
  Surrogate.prototype = parent.prototype;
  child.prototype = new Surrogate;

  // Add prototype properties (instance properties) to the subclass,
  // if supplied.
  if (protoProps) _.extend(child.prototype, protoProps);

  // Set a convenience property in case the parent's prototype is needed
  // later.
  child.__super__ = parent.prototype;

  return child;
}

というわけで、子で親クラスのメソッドを使いたい場合は、

var Parent = Backbone.Model.extend({
  c: function() { console.log('Parent'); }
});

var Child = Parent.extend({
  c: function() {
    console.log('Child');
    this.constructor.__super__.c(); // => "Parent"
  }
});

ってすればできる。

そもそもなんで

こんなことに今さら気付いたかというと、
CoffeeScriptで書かれたコードを、生JavaScriptのプロジェクトで使いたくって、コンパイルして参考にしようとしたときの話です。

CoffeeScriptの継承イディオムって、コンパイル(そして整形)するとこうなってるんですよね。

var A, B,
    __hasProp = {}.hasOwnProperty,
    __extends = function(child, parent) {
        for (var key in parent) {
            if (__hasProp.call(parent, key))
                child[key] = parent[key];
        }
        function ctor() {
            this.constructor = child;
        }
        ctor.prototype = parent.prototype;
        child.prototype = new ctor();
        child.__super__ = parent.prototype;
        return child;
    };

A = (function() {
  function A() {}

  return A;

})();

B = (function(_super) {
  __extends(B, _super);

  function B() {
    return B.__super__.constructor.apply(this, arguments);
  }

  return B;

})(A);

prototypeちゃんと使えるようになってるし、これで十分なのでは?って思ってたけど、
そもそも親に同名の定義があったらどうやって呼ぶのとか、諸々考えるべきことがあるらしく。
そういうわけでJavaScriptは世間を騒がせるんですね・・。

__super__とかconstructorとか、そういうもんってわかってからはだいぶスッキリしてるので、
なんかひとつかしこくなった感すらあります。

うーむ、勉強させていただきます。
@dameleon++

参考:CoffeescriptとTypescriptから学ぶjsでのクラス・継承パターン | WEB EGG