JavaScript継承パターンまとめ

プロトタイプ

function Animal(){};
Animal.prototype =
{
    sleep : function(){},
    walk : function(){ alert('noshi, noshi') }
};
function Human(){};
Human.prototype = new Animal();

new Human().walk(); // noshi, noshi
  • もっともポピュラだと思われる。

変型プロトタイプ

Human.prototype.__proto__ = Animal.prototype;
new Human().walk(); // noshi, noshi
  • ほとんどのIEを除くJavaScript処理系で実行可能。裏ワザちっく。

エクステンド

/**
 * extend function
 * @param {Object} s superclass
 * @param {Function} c constructor
 */
function extend(s, c)
{
	function f(){};
	f.prototype = s.prototype;
	c.prototype = new f();
	c.prototype.__super__ = s.prototype;    // __super__のところを superclass とかにしてもOK!!
         c.prototype.__super__.constructor = s;  // 上に同じく。但し、 super は予約語。
	c.prototype.constructor = c;
	return c;
};

Human = extend(Animal, function()
{
    // Human コンストラクタ
    // this.__super__.constructor(); ← でAnimalコンストラクタ呼出せる。
});

Human.prototype.walk = function()
{
   this.__super__.walk();
   alert('teku, teku');
}

new Human().walk(); // alert('noshi, noshi')  alert('teku, teku');
  • プロトタイプの拡張
  • YUIでも似た実装されているよ。

クローン

function clone(s)
{
   function f(){};
   f.prototype = s;
   return new f();
}

Animal = { sleep : function(){ alert('zzz...') } };
Human = clone(Animal);
Human.walk = function(){ alert('teku, teku') };

for(i in Animal) alert(i); // sleep だけ
for(i in Human) alert(i);  // sleep, walk

デファイン

/**
 * @param {String} s スーパクラス名(同一名前空間にある必要ある。)
 * @param {Function} c コンストラクター function Hoge() みたいに名前つける。
 * @param {Object} o メソッドとかプロパティ
 */
function define(s, c, o)
{
    var i, p, n = c.toString().match(/^function ([^(]+)\(/)[1];
    p = c.prototype = new this[s];
    for(i in o){ p[i] = o[i]; };
    this[n] = c;
}

namespace = {};
namespace.define = define;
namespace.Animal = function(){};
namespace.Animal.prototype = { prop1: 'Animal' };

namespace.define
(
    'Animal',
    function Human()
    {
        // 何かする。
    },
    {
       prop2 : 'Human'
    }
);

alert(new namespace.Human().prop1); // Animal

冗長に見えるけどあるオブジェクトを名前空間のように利用した場合には以下みたいにコード書かなくていいのでらくだと思います...。

foo = {};
foo.bar = {};
foo.bar.define = define;
foo.bar.Animal = function(){};
foo.bar.Animal.prototype = {};

// - foo.bar.Human.prototypeが続く タイポにつながる?
foo.bar.Human = function(){};
foo.bar.Human.prototype = new foo.bar.Animal();
foo.bar.Human.prototype.prop1 = 1;
foo.bar.Human.prototype.prop2 = 2;

// - まぁ人の好みですけど...。
foo.bar.define
(
    'Animal',
    function Human()
    {
    },
    {
        prop1 : 1,
        prop2 : 2
    }
);

ビルトインString

ArrayやStringオブジェクトの継承。valueOfとtoStringをハックしてあげるのがポイント。

function MyString(s)
{
   function toString(){ return s.toString(); }
   this.valueOf = toString;
   this.toString = toString;
}
MyString.prototype = new String();
MyString.prototype.bigChar = function(l){
    return Array(l+1).join(this);
}

str = new MyString('foo');
alert(str.bigChar(2)); // foo foo

最後に

以上、プロトタイプ・エクステンド・クローン・デファイン・ビルトインStringの4+1パターン。名前は勝手につけましたので...正しいのがある場合は指摘下さい。JavaScriptの継承はまぁ要は、実装次第ということです。

追記(02/23)

ビルトインパターン。実際はStringしか(Numberは動いているように見える)動かないことが判明しました。この記事書いたときに実際に試したんですが検証の仕方が誤っていたようです。以下誤った検証。

//誤った検証例でfxで確認
function myArray(){};
myArray.prototype new Array();

a = new myArray();
a.push("Hello");
a.push("World!!");

alert(a.join(",")); // Hello,World!! ← fxやoperaではいいけど。IEでは空欄

// -- ここでかつfxのみで検証完了してました。

b = new myArray();
b[0] = "Hello";
b[1] = "World!!";

alert(b.join(",")); // 【空欄】

なんとなく。結合できる何か…

// 結合できる何か。
function Joinable(a)
{
    a = Array.prototype.slice.apply(a);
    for(var i=0,f=a.length;i<f;i++){
        this[i] = a[i];
    };
    this.length = a.length;
};

Joinable.prototype.length = 0;

Joinable.prototype.join = function(){
    return Array.prototype.join.apply(this, arguments);
};

Joinable.prototype.set = function(v){
    this[this.length++] = v;
};

var a = ["Hello","World"];

b = new Joinable(a);
b.set("!!");

alert(b.join(",")); // Hello,World,!!