指向性メモ::2005-07-24::クロージャベース・オブジェクト指向による JavaScript の記事にあったものを改良し、汎用的に使えるようにしてみた。
Object.prototype.createChild = function(source){
function Temp(){};
Temp.prototype = this;
var childObject = new Temp;
if(source instanceof Object){
childObject.extend(source);
}
return childObject;
}
Object.prototype.extend = function(source , deeply){
if(!(source instanceof Object)){
throw new Error;
}
for(var property in source){
if((deeply || source.hasOwnProperty(property)) && this[property] !== source[property]){
this[property] = source[property];
}
}
return this;
}
Function.prototype.inherit = function(superclass){
function Temp(){};
Temp.prototype = superclass.prototype;
return this.prototype = new Temp;
};
var Class = (function(){
function createSubclass(closure){
if(!(closure instanceof Function)){
throw new Error;
}
var newClass = function(){
var toInherit = (this == newClass);
var constructor;
if(toInherit){
constructor = arguments[0];
}else{
constructor = newClass;
}
var superclass = newClass.prototype.constructor;
var inheritance = superclass.call(superclass , constructor);
var published = inheritance.published;
closure.call(published , inheritance.privileged);
if(toInherit){
return inheritance;
}else{
published.constructor = newClass;
if(published.initialize !== undefined){
published.initialize.apply(published , arguments);
}
return published;
}
}
newClass.inherit(this);
newClass.createSubclass = createSubclass;
return newClass;
}
return {
create : function(closure){
if(!(closure instanceof Function)){
throw new Error;
}
var newClass = function(){
var toInherit = (this == newClass);
var constructor;
if(toInherit){
constructor = arguments[0];
}else{
constructor = newClass;
}
var privileged = {};
var published = constructor.prototype.createChild();
closure.call(published ,privileged);
if(toInherit){
return {privileged : privileged , published : published};
}else{
if(published.initialize !== undefined){
published.initialize.apply(published , arguments);
}
return published;
}
}
newClass.createSubclass = createSubclass;
return newClass;
}
}
})();
利用法は以下の通り。
var someClass = Class.create(function(privileged){
var privateVariable = ... ;
var privateMethod = function(){...};
privileged.protectedVariable = ... ;
privileged.protectedMethod = function(){...};
this.publicVarible = ... ;
this.publicMethod = function(){...};
});
someClass.prototype.publicProperty = ... ;
var Subclass = someClass.createSubclass(function(privileged){
//スーパークラスのメソッドを呼び出す場合はこうする
var superProtectedMethod = privileged.protectedMethod;
privileged.protectedMethod = function(){
superProtectedMethod();
... ;
};
... ;
});
var instance = new someClass;
クラスやサブクラスを作る場合は、Class.create や someClass.createSubclass の引数として Function オブジェクトを渡す。渡した関数内で宣言された変数が private になり、渡す関数の第一引数のプロパティが protected になる。this は通常のやり方でコンストラクタを使ったときと同じく生成されるオブジェクトそのものを表し、プロパティは public になる。また、通常のやり方にそって someClass.prototype にプロパティを追加することもできる。ただし、この場合は private や protected な変数を使うことはできない。
また、instanceof や instance.constructor も通常通り使える。
var someClass = Class.create(function(){});
var Subclass = someClass.createSubclass(function(){});
var instance = new Subclass;
//printは適当に定義
print(instance.constructor == Subclass);//true
print(instance instanceof Subclass);//true
print(instance instanceof someClass);//true
使用例
var Product = Class.create(function(){
this.extend({
initialize : null,
getID : null
});
});
var Factory = Class.create(function(privileged){
this.extend({
initialize : null,
create : function(id){
var product = privileged.createProduct(id);
privileged.registerProduct(product);
return product;
}
});
privileged.extend({
createProduct : null,
registerProduct : null
});
});
var ConcreteProduct = Product.createSubclass(function(){
var id;
this.extend({
initialize : function(_id){
id = _id;
},
getID : function(){
return id;
}
});
});
var ConcreteFactory = Factory.createSubclass(function(privileged){
var IDList = [];
this.extend({
initialize : undefined,
each : function(func){
for(var i = 0; i < IDList.length; i++){
func(IDList[i]);
}
}
});
privileged.extend({
createProduct : function(id){
return new ConcreteProduct(id);
},
registerProduct : function(product){
IDList.push(product);
}
});
});
var factory = new ConcreteFactory;
factory.create('001');
factory.create('002');
factory.create('003');
//print()は処理系に合わせて定義してください。
factory.each(function(x){print(x.getID());});
/*
001
002
003
*/
注意点
-
インスタンス生成時に public な initialize メソッド (instance.initialize) が実行されるが、この initialize メソッドは public 固定で、protected や private にすることは出来ない。
アクセスレベルを指定できるようにできないこともないのだが、どうしても書き方が今以上に複雑になる方法しか思いつかず、断念した。
-
protected な変数を複数定義するとき、
var someClass = Class.create(function(privileged){
privileged.someVariable = someValue;
privileged.someMethod = function(){...};
});
上記のような記述を略して、下記のような記述にしてはいけない。
//これだとうまくいかない
var someClass = Class.create(function(privileged){
privileged = {
someVariable : someValue,
someMethod : function(){...}
};
});
変数 privileged に最初存在した、protected な変数をプロパティとしたオブジェクトへの参照が上書きされてしまうからだ。略したい場合はこうする。
var someClass = Class.create(function(privileged){
privileged.extend({
someVariable : someValue,
someMethod : function(){...}
});
});
同様にクラスの someClass.prototype のプロパティを複数定義するときも、略記する際には、
var someClass = Class.create(function(){...});
someClass.prototype.extend({
someProperty : ... ,
...
});
上記のようにする。
-
ECMAScript は状況によって this の値が変わる 。生成されるインスタンスへの参照であることを確実に示したいなら、クラスの定義の冒頭で、
var someClass = Class.create(function(){
var published = this;
... ;
});
などどし、以後変数 published を this の代わりに使うとよい。
デメリット
- 書き方が複雑。
- インスタンス毎にメソッドをもつため、メモリの効率が悪い。
参考リンク
指向性メモ::2005-07-24::クロージャベース・オブジェクト指向による JavaScript
別のやり方で変数の隠蔽を実現しているところ
Function.prototype.inherit の元ネタ
Starry Night - Diary - 2001/12/20 JavaScript 継承3
fladdict.net blog: JavaScript, ActionScriptにおける .this とは何なのか?
継承されたか、new されたかの判別方法が手抜きなので直さねばと思っているが、やる気が起きない。