JavaScript で private/public の実現
JavaScript は意外に強力な言語である。
http://d.hatena.ne.jp/brazil/20050829/1125321936
Private Members in JavaScript
これらの記事に触発されて、prototype.js風にprivate/public機能をJavaScriptで実現してみた。こんな風にかける。
var Hoge = LayeredClass.create(); Hoge.prototype = { __private: { privateMethod: function(){ alert("public:" + this.publicVariable + "; private:" + this.privateVariable); } }, __public:{ publicVariable: null, initialize: function(){ this.publicVariable = 10; this.privateVariable = 20; }, publicMethod: function(){ this.privateMethod(); } } }
つまり、__privateの下にprivateなメンバメソッドを記述して、__publicの下にpublicなメンバ変数とメンバメソッドを記述する。このとき、通常のJavaScriptと異なり、publicなメンバ変数を明示的に記述する必要があることに注意。また、メソッド内で定義されたメンバ変数は全てprivateとなる。なお、コンストラクタはprototype.jsと同様に__public:initializeである。
このように Hoge クラスを定めると、Hogeクラスのprivateな変数やメソッドには、Hogeオブジェクトの外部からはアクセスできなくなる。つまり、次のようになる。
var hoge = new Hoge(); hoge.publicMethod(); // -> "public:10; private:20" alert(hoge.publicVariable); // -> "10" alert(hoge.privateVariable); // -> "undefined" hoge.privateMethod(); // -> error
LayeredClassのソースはこちら。
var LayeredClass = { layered: true, create: function() { var privateClass = function(){}; privateClass.prototype = undefined; var __initilizePrototypeHandler; var __layered = this.layered; var __class = function() { if(privateClass.prototype == undefined){ __initilizePrototypeHandler(this); } if(__layered && Object.__defineSetter__){ this.__privateInstance = new privateClass; } this.initialize.apply(this, arguments); } __initilizePrototypeHandler = function(self){ if(__layered && Object.__defineSetter__){ privateClass.prototype = {}. extend( self.__public ).extend( self.__private ); for( property in self.__public ){ if( property == "extend" ){ continue; } if(typeof self.__public[property] == "function"){ __class.prototype[property] = function(__p){ return function(){ return this.__public[__p]. apply(this.__privateInstance, arguments); } }(property); }else{ __class.prototype.__defineSetter__(property, function(__p){ return function(__x){ this.__privateInstance[__p] = __x; } }(property) ); __class.prototype.__defineGetter__(property, function(__p){ return function(){ return this.__privateInstance[__p]; } }(property) ); } } }else{ privateClass.prototype = {}; __class.prototype.extend( self.__public ).extend( self.__private ); } }; return __class; } }
一応ライセンスはprototype.jsと同じで。なお、prototype.jsのObject.extendを使っているので、prototype.jsを利用するか、上のソースの前のほうに、
Object.extend = function(destination, source) { for (property in source) { destination[property] = source[property]; } return destination; } Object.prototype.extend = function(object) { return Object.extend.apply(this, [this, object]); }
を追加しておくこと。
ただし、private/publicが機能するのはmozilla系ブラウザだけである。これは、__defineSetter__ / __defineGetter__を使っているためである。とはいえ、それ以外のブラウザでもスクリプトの動作に問題はない。mozilla系以外のブラウザでは、全てのメンバがpublicとなり、privateメンバの外部からの呼び出しが可能になるだけである。
なお、LayeredClass.layeredをfalseにすれば、全てのブラウザで、private/public機能はオフになる。private/public機能をオンにしておくと、メソッド呼び出しの際にオーバーヘッドが発生するので、private/public機能が必要な場合(開発中の場合など)のみLayeredClass.layeredをtrueにすべきである。
以上をまとめると、開発の流れとしては次のようになると思う。
- LayeredClass.layeredをtrueにして、主にmozilla系ブラウザで動作確認。privateメソッドが外部から呼び出されていないことを確認する。
- LayeredClass.layeredをfalseにしてリリース
LayerdClass の実装の詳細については、また、後日。