Chapter 6. Code Reuse Patterns
2011/02/23(水) 25:45 Javascript親記事へこのエントリーをはてなブックマークに追加

コードの再利用について。
こう言ったときに継承が思い浮かぶかもしれないがそれは手段の一つにすぎない。

Classical Versus Modern Inheritance Patterns

JavaScriptにはクラスという概念はありませんが、クラスと似たような事は行えます。
//In Java you could do something like:
Person adam = new Person();
//In JavaScript you would do:
var adam = new Person();
上記のようにJavaのようにPerson型のadmというインスタンスを宣言できます。
このようなクラスは “classical.”といえるでしょう、またその他のクラスについて考える必要ないパターンは“modern”といえるのではないでしょうか。
なので、 modern patternにより理解を深めることが重要。

Classical Pattern #1—The Default Pattern

Following the Prototype Chain

prototypeチェーンの解説

Drawbacks When Using Pattern #1

Classical Pattern #2—Rent-a-Constructor

// a parent constructor
function Article() {
    this.tags = ['js', 'css'];
}
var article = new Article();
// a blog post inherits from an article object
// via the classical pattern #1
function BlogPost() {}
BlogPost.prototype = article;
var blog = new BlogPost();
// note that above you didn't need `new Article()`
// because you already had an instance available
// a static page inherits from article
// via the rented constructor pattern
function StaticPage() {
    Article.call(this);
}
var page = new StaticPage();
alert(article.hasOwnProperty('tags')); // true
alert(blog.hasOwnProperty('tags')); // false
alert(page.hasOwnProperty('tags')); // true
上記のコードからわかるようにArticle()を継承する方法は二つある。
一つ目のパターンはprototypeを通じてtagsプロパティにアクセスするため、blog.hasOwnProperty('tags');//falseになる。
pageのパターンは参照ではなくコピーが得られるためpage.hasOwnProperty('tags'); // trueとなる。そのため、page.tagsに変更を加えてもarticle.tagsには影響しない。

The Prototype Chain

先ほどのpageのパターンを少し変えて、以下のようにapplyを使えば引数を渡せるようになる
この方法は複数のものを継承させようとした場合も同様にapplyしていけばいいだけ。
// the parent constructor
function Parent(name) {
    this.name = name || 'Adam';
}
// adding functionality to the prototype
Parent.prototype.say = function () {
    return this.name;
};
// child constructor
function Child(name) {
    Parent.apply(this, arguments);
}
var kid = new Child("Patrick");
kid.name; // "Patrick"
typeof kid.say; // "undefined"

Pros and Cons of the Borrowing Constructor Pattern

このConstructor Patternの特徴は、prototypeからは何も引き継がれないのが欠点ですが、親のコピーを継承するので親のプロパティをいじっても大丈夫という利点もあります。(同じ事)

Classical Pattern #3—Rent and Set Prototype

Constructor Patternをさらに発展させて、prototypeも継承させてみましょう。
以下のようにchildの prototype はコンストラクタのインスタンスを示すようにします。
function Child(a, c, b, d) {
    Parent.apply(this, arguments);
}
Child.prototype = new Parent();
この方法では親のコピーと参照を得られる訳です。
欠点としては二回も親を呼んでいるため効率が良くないことがあげられます。
(コピーと参照で二回)
// the parent constructor
function Parent(name) {
    this.name = name || 'Adam';
}
// adding functionality to the prototype
Parent.prototype.say = function () {
    return this.name;
};
// child constructor
function Child(name) {
    Parent.apply(this, arguments);
}
Child.prototype = new Parent();
var kid = new Child("Patrick");
kid.name; // "Patrick"
kid.say(); // "Patrick"
delete kid.name;// 削除してもprototypeはあるため
kid.say(); // "Adam"

Classical Pattern #4—Share the Prototype

先ほどのパターンだと二回の呼び出しが必要になっていたので、次は親を呼ぶことなく継承してみましょう。
function inherit(C, P) {
    C.prototype = P.prototype;
}
子(C)の__proto__が親(P)のprototypeを示すという感じ。(C.__proto__ → P.prototype ← P.__proto__)
でもこれは子がprototypeを書き換えるとが親などにも影響が出てしまいます。

Classical Pattern #5—A Temporary Constructor

先ほどのは直接C.__proto__がP.prototypeを参照してしまうため書き換えの危険性がありました。
そのため、間にfunction F()というものを挟んでC.__proto__がF.prototypeを参照するようにします。
C.__proto__ → Fのインスタンス、F.__proto__ → P.prototype ← P.__proto__
function inherit(C, P) {
    var F = function () {};
    F.prototype = P.prototype;
    C.prototype = new F();
}

Storing the Superclass

いわゆるsuperクラスの実装
uberから親のメンバを参照できる。
function inherit(C, P) {
    var F = function () {};
    F.prototype = P.prototype;
    C.prototype = new F();
    C.uber = P.prototype;
}

Resetting the Constructor Pointer

先ほどのパターンで、以下のようなコードからconstructorの挙動がおかしい事がわかるでしょう。
// parent, child, inheritance
function Parent() {}
function Child() {}
inherit(Child, Parent);
// testing the waters
var kid = new Child();
kid.constructor.name; // "Parent" Child であるべき
kid.constructor === Parent; // true
そのためconstructorプロパティも書き換えるようにします。
function inherit(C, P) {
    var F = function () {};
    F.prototype = P.prototype;
    C.prototype = new F();
    C.uber = P.prototype;
    C.prototype.constructor = C;
}
// test
function Parent() {}
function Child() {}
inherit(Child, Parent);
// testing the waters
var kid = new Child();
kid.constructor.name; // "Child"
kid.constructor === Parent; // false
このパターンは最終的に以下のようなinherit関数にまとめる事ができます。
var inherit = (function () {
    var F = function () {};
    return function (C, P) {
        F.prototype = P.prototype;
        C.prototype = new F();
        C.uber = P.prototype;
        C.prototype.constructor = C;
    }
}());

Klass

JavaScriptライブラリの多くはClassをエミュレートしたものを導入しています。
それぞれ実装は異なったりしますが、共通している部分もあります。
  • initialize, _initなど自動的に呼び出されるものがある
  • 他のクラスから継承
  • superclassというように親のクラスにアクセスできる
いわゆるklass()という感じで実装されている例
var Man = klass(null, {
    __construct: function (what) {
        console.log("Man's constructor");
        this.name = what;
    },
    getName: function () {
        return this.name;
    }
});
klassはどういう実装になっているか。
var klass = function (Parent, props) {
    var Child, F, i;
    // 1.
    // new constructor
    Child = function () {
        if (Child.uber && Child.uber.hasOwnProperty("__construct")) {
            Child.uber.__construct.apply(this, arguments);
        }
        if (Child.prototype.hasOwnProperty("__construct")) {
            Child.prototype.__construct.apply(this, arguments);
        }
    };
    // 2.
    F = function () {};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.uber = Parent.prototype;
    Child.prototype.constructor = Child;
    // 3.
    // add implementation methods
    for (i in props) {
        if (props.hasOwnProperty(i)) {
            Child.prototype[i] = props[i];
        }
    }
    // return the "class"
    return Child;
};

Prototypal Inheritance

モダンなprototype継承について。
function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

Addition to ECMAScript 5

ES5ではObject.create()が使えるのでこのパターンは単純に以下のように書く事ができます。
var child = Object.create(parent);

Inheritance by Copying Properties

次はコピーする事で継承する行く方法を見てみましょう。
function extend(parent, child) {
    var i;
    child = child || {};
    for (i in parent) {
        if (parent.hasOwnProperty(i)) {
            child[i] = parent[i];
        }
    }
    return child;
}
// test
var dad = {name: "Adam"};
var kid = extend(dad);
kid.name; // "Adam"
このコピーは “shallow copy”と言われる実装で、deep copyではありません。
そのため以下のように参照しているのがわかります。
var dad = {
    counts: [1, 2, 3],
    reads: {paper: true}
};
var kid = extend(dad);
kid.counts.push(4);
dad.counts.toString(); // "1,2,3,4"
dad.reads === kid.reads; // true
これをdeep copyに変えた実装をしてみると
function extendDeep(parent, child) {
    var i,
        toStr = Object.prototype.toString,
        astr = "[object Array]";
    child = child || {};
    for (i in parent) {
        if (parent.hasOwnProperty(i)) {
            if (typeof parent[i] === "object") {
                child[i] = (toStr.call(parent[i]) === astr) ? [] : {};
                extendDeep(parent[i], child[i]);
            } else {
                child[i] = parent[i];
            }
        }
    }
    return child;
}
このdeep copyは jQueryのextend() やYUI3のY.clone()という形で実装されています。

Mix-ins

任意のオブジェクトをmixするものを考えてみる
function mix() {
    var arg, prop, child = {};
    for (arg = 0; arg < arguments.length; arg += 1) {
        for (prop in arguments[arg]) {
            if (arguments[arg].hasOwnProperty(prop)) {
                child[prop] = arguments[arg][prop];
            }
        }
    }
    return child;
}
// test
var cake = mix(
   {eggs: 2, large: true},
   {butter: 1, salted: true},
   {flour: "3 cups"},
   {sugar: "sure!"}
);
*Borrowing Methods
**Example: Borrow from Array
>|javascript|
function f() {
    var args = [].slice.call(arguments, 1, 3);
    return args;
}
// example
f(1, 2, 3, 4, 5, 6); // returns [2,3]

Borrow and Bind

call()/apply()のこと

Function.prototype.bind()

ES5ではFunction.prototype.bind()が提供されているのでそれを利用できます。
以下のように似たような実装をすることはできます。
if (typeof Function.prototype.bind === "undefined") {
    Function.prototype.bind = function (thisArg) {
        var fn = this,slice = Array.prototype.slice,
            args = slice.call(arguments, 1);
        return function () {
            return fn.apply(thisArg, args.concat(slice.call(arguments)));
        };
    };
}

さいごに

継承の手順を順番に紹介していたので結構わかりやすかった 

名前:  非公開コメント   

  • TB-URL  http://efcl.info/adiary/098/tb/