はじめに
javascriptでの継承の基本パターン4つという記事で、javascriptの継承パターンを整理してみたのですが、忘れないうちにライブラリ化してしまおうと思い。jQuery.extend()風のインターフェースを持つメソッド群を作ってみました。
あえて、クラスの概念を使わず、でも、必要に応じてクラス相当のことができるという実装になっているのが、この手のライブラリとしては新しいかと思います。extendのインターフェースに慣れた人にとっては、クラス系のライブラリよりシンプルで使いやすいのではないかと思います。
コード
(function ($){
// private variable and functions
var slice = Array.prototype.slice;
function getNamedFunction(funcName){
return (new Function(
"return function "
+ (funcName || "")
+ "(){return this.__init.apply(this, arguments);}"
))();
}
function isString(str){
return typeof str == 'string' || str instanceof String;
}
$.extend({
// Usage: jQuery.inherit(proto, [prop1, [prop2 ...]])
inherit : (function (){
if (typeof Object.create === 'function'){
return function (proto){
var args = slice.call(arguments, 1);
return $.extend.apply($, [Object.create(proto)].concat(args));
}
}
else{
return function (proto){
var args = slice.call(arguments, 1),
Temp = function () {};
Temp.prototype = proto;
return $.extend.apply($, [Temp()].concat(args));
}
}
}()),
// Usage: jQuery.inheritPrototype([funcName], proto, [prop1, [prop2 ...]])
inheritPrototype : function (){
var firstIsString = isString(arguments[0]),
funcName = firstIsString ? arguments[0] : '',
proto = arguments[firstIsString ? 1 : 0],
args = slice.call(arguments, firstIsString ? 2 : 1),
Constructor = getNamedFunction(funcName);
Constructor.prototype = $.inherit.apply($, [proto].concat(args, [{
__inherited : proto,
constructor : Constructor
}]));
return Constructor;
},
// Usage: jQuery.extendPrototype([funcName], [prop1, [prop2 ...]])
extendPrototype : function (){
var firstIsString = isString(arguments[0]),
funcName = firstIsString ? arguments[0] : '',
args = slice.call(arguments, firstIsString ? 1 : 0),
Constructor = getNamedFunction(Constructor);
Constructor.prototype = $.extend.apply($, [{}].concat(args, [{
constructor : Constructor
}]));
return Constructor;
}
});
})(jQuery);
解説
前の記事で書いたように、javascriptの継承パターンには4つあります。そのうち一つは、$.extend(uber, prop1, prop2....)とすれば良いので、残りの3パターンを実装しました。継承そのものの技術的側面については前の記事を見てもらうとして、主に利用方法について説明します。
jQuery.inherit(proto, prop1, prop2...) は、__proto__を使った継承のコアなロジックを
提供するもので、protoを継承する(つまり、__proto__として持つ)新しいオブジェクトを生成した上で、prop1, prop2以下でextendします。内部的には、Object.createを使える場合は、Object.createを使い、そうじゃない場合は前の記事で紹介したような、一時コンストラクタを使った継承をしています。
jQuery.extendPrototype(prop1, prop2...) は、jQuery.extendと似ていますが、コンストラクタを作ることが違います。新しいオブジェクトをprop1, prop2.. でexntedしたオブジェクトをprototypeとして持つコンストラクタを返します。クラスの継承として使う場合は、prop1としてスーパークラスのprototype、prop2として実装内容をオブジェクトとして渡します。
jQuery.inheritPrototype(proto, prop1, prop2)_ は、jQuery.inheritの2つの組み合わせです。protoを継承し(つまり、protoを__proto__に持ち)、prop1, prop2...で拡張したオブジェクトをprototypeとして持つコンストラクタを返します。クラスの継承として使う場合は、propとしてスーパークラスのprototype、prop1として実装内容をオブジェクトとして渡します。
ちなみに、extendPrototype, inheritPrototypeは、最初の引数として文字列を与えると、デバッグ時、オブジェクト名としてその名前が表示されるようになります。なくても動作しますが、入れておくと便利です。内部的には、Functionオブジェクトで動的にコードを生成しています。
extendPrototype, inheritPrototypeは、通常クラスの継承に使われることになると思いますが、設計上は、クラスの概念を使っていないのが特徴で、クラスを継承する場合も、スーパークラスを指定するのではなく、スーパークラスのprototypeを渡すようになっているのはそのためです。このため、クラス/plainオブジェクトを区別せずに扱えるjavascriptらしい設計になっています。
テストコード兼サンプル
すごく適当です。足りない条件がいろいろありそうなので、気づいた人は教えていただけると幸いです。
var Animal = $.inheritPrototype("Animal", null, {
__init: function(message){
console.log(this.name + ' born on earch. ' + message)
},
getWeight: function(){
return this.weight++;
},
getName: function(){
return this.name;
},
weight: 0,
name: 'anonumous'
});
var human1 = $.inherit(new Animal(), {weight: 10});
console.log(human1.getWeight() == 10 ? 'OK' : 'NG');
console.log(human1.getWeight() == 11 ? 'OK' : 'NG');
console.log(human1.getName() == 'anonumous' ? 'OK' : 'NG');
var human2 = $.extend(new Animal(), {weight: 20});
console.log(human2.getWeight() == 20 ? 'OK' : 'NG');
console.log(human2.getWeight() == 21 ? 'OK' : 'NG');
console.log(human2.getName() == 'anonumous' ? 'OK' : 'NG');
var Human3 = $.extendPrototype(Animal.prototype, {weight: 30});
human3 = new Human3('Hello');
console.log(human3.getWeight() == 30 ? 'OK' : 'NG');
console.log(human3.getWeight() == 31 ? 'OK' : 'NG');
console.log(human3.getName() == 'anonumous' ? 'OK' : 'NG');
var Human4 = $.inheritPrototype(Animal.prototype, {weight: 40});
human4 = new Human4('Hello');
console.log(human4.getWeight() == 40 ? 'OK' : 'NG');
console.log(human4.getWeight() == 41 ? 'OK' : 'NG');
console.log(human4.getName() == 'anonumous' ? 'OK' : 'NG');
var Human5 = $.extendPrototype("Human", Animal.prototype, {weight: 30});
human5 = new Human5('Hello');
console.log(human5.getWeight() == 30 ? 'OK' : 'NG');
console.log(human5.getWeight() == 31 ? 'OK' : 'NG');
console.log(human5.getName() == 'anonumous' ? 'OK' : 'NG');
var Human6 = $.inheritPrototype("Human", Animal.prototype, {weight: 40});
human6 = new Human6('Hello');
console.log(human6.getWeight() == 40 ? 'OK' : 'NG');
console.log(human6.getWeight() == 41 ? 'OK' : 'NG');
console.log(human6.getName() == 'anonumous' ? 'OK' : 'NG');
ちなみに、
var human1 = $.extend(new Animal(), {weight: 10});
のところで、new Animal()をプレインオブジェクトに変えることもできますが、その場合は$.extend({}, proto, {weight: 10})のようにしないとおかしなことになります。
jQuery
jQueryオブジェクトを使っていないのに、ほぼ関数のホルダとしてjQueryを使っていること、jQueryの名前空間を汚染しまくっていることを訝しく思う方もいらっしゃるかと思いますが、適当に脳内で変換して読んでやってください。