prototype.jsでプライベートメソッドを持つクラスを作る方法
Private Members in JavaScript
[翻訳]JavaScriptにおけるプライベートメンバ(COLLECTION & COPY)
上記の記事に刺激を受けて、prototype.jsでプライベートメソッドを持つクラスを作る方法を考えてみました。
JavaScriptのクロージャを利用してますが、具体的には次のようになります。
1: //Animalクラス作成
2: var Animal = Class.create(); 3: 4: //Animalクラスフィールド設定
5: Animal.MALE = 0; 6: Animal.FEMALE = 1;7: Animal.SEXTYPE = ['Male','Female'];
8: 9: //Animalクラス設定
10: Animal.prototype = { 11: initialize : function(_sex) {12: this.define(_sex);
13: }, 14: 15: define : function(_sex) {16: //プライベートフィールド
17: var sex = _sex; 18: 19: //プライベートメソッド
20: var _getSex = function() {21: return sex;
22: } 23: 24: //Privileged(特権)メソッド
25: this.getSex = function (){
26: return Animal.SEXTYPE[_getSex()];
27: } 28: } 29: }
ポイントの1つ目は、Animal#defineメソッド内のローカル変数として、sexを定義していることです(17行目)。メソッド内のローカル変数ですので、当然外部からは呼び出せず、プライベートフィールドとして扱うことができます。
次のポイントは、Animal#defineメソッド内で、_getSexというメソッドを記述していることです(20行目)。_getSexメソッドは、プライベートメソッドとして動作します。また、_getSexメソッドでは、Animal#defineメソッド内のローカル変数にアクセスすることができます。不思議な感じですが、この辺がJavaScriptのクロージャという機能になります。
最後のポイントは、Animal#getSexメソッドです(25行目)。Animal#getSexメソッドは、Animal#defineメソッド内の関数ですが、thisオブジェクトに関数を追加しています。そのため、Animal#defineメソッド内のプライベートフィールド・プライベートメソッドにアクセス可能な上に、外部から呼び出し可能なパブリックメソッドになります。この特殊なメソッドを、元記事では「Privileged(特権)メソッド」と呼んでいます。
このAnimalクラスを実行すると、次のようになります。
1: var animal = new Animal(Animal.MALE);
2: //alert(animal.sex); // undefined
3: //alert(animal._getSex()); // Error
4: alert(animal.getSex()); // Maleプライベートフィールドにも、プライベートメソッドにもアクセスできず、Privileged(特権)メソッドは問題なく実行できています。
これだけではおもしろくないので、継承についても考えてみました。
1: //Personクラス生成
2: var Person = Class.create();
3: 4: //Animalクラスフィールドコピー
5: copyMembers(Person, Animal); 6: 7: //Personクラス設定
8: Person.prototype = {9: initialize : function(_name, _sex) {
10: this.define(_name, _sex);
11: }, 12: 13: define : function(_name, _sex) {
14: 15: //Animalクラスのdefineメソッドを実行
16: var func = eval(Animal.prototype.define);
17: func.apply(this, [_sex]);
18: 19: //プライベートフィールド
20: var name = _name;
21: 22: //プライベートメソッド
23: var _getName = function(){
24: return name;
25: } 26: 27: //Privileged(特権)メソッド
28: this.getName = function() {
29: return _getName();
30: } 31: } 32: } 33: 34: //クラスメンバをコピー
35: function copyMembers(_target, _from) {
36: var exclude_methods
= ['prototype','bind','bindAsEventListener'];
37: 38: for (var member in _from) {
39: var key = member.toString();
40: if (exclude_methods.indexOf(key) == -1) {
41: _target[key] = _from[key]; 42: } 43: } 44: }
ポイントの1つ目は、5行目でcopyMembers関数を実行しているところです。
通常のオブジェクト指向では、インスタンスフィールドやインスタンスメソッドだけではなく、クラスフィールドやクラスメソッドも継承します。しかし、prototype.jsでは、prototypeしかコピーしないため、インスタンスフィールドとインスタンスメソッドしか継承できません。
これにはどうも違和感があったので、このcopyMembers関数を使用して、AnimalクラスのクラスフィールドとクラスメソッドをPersonクラスに継承できるようにしています。
次のポイントは、prototype.jsを使っているのに、Object.extendを使用した継承を行っていない点でしょうか。
最後のポイントは、16~17行目です。この2行でAnimal#defineメソッドを実行して、Animalクラスで実装している内容を、Personクラスでも実装します。これは、Animalクラスの実装を、Animal#defineメソッドに集約させているために可能になります。このため、Object.extendを使用しなくても継承を行なうことができています。
Personクラスを実行すると、以下のようになります。
1: var taro = new Person('Taro',Person.MALE);
2: //alert(taro.name); // undefined
3: //alert(taro._getName()); // Error
4: alert(taro.getName()); // Taro
5: alert(taro.getSex()); // Male
6: 7: var ellen = new Person('Ellen',Person.FEMALE);
8: //alert(ellen.name); // undefined
9: //alert(ellen._getName()); // Error
10: alert(ellen.getName()); // Ellen
11: alert(ellen.getSex()); // Female
1行目では、Person.MALEを使用できているので、Animalクラスからのクラスフィールドの継承がうまくいっていることが分かります。
また、5行目では、Animalクラスのメソッド実行がうまくいっているので、スーパークラスのメソッド実行も問題なくできています。
JavaScriptでカプセル化はできないと思っていましたが、やりようによっては結構できるものですね。
なお、この記事のコードは、IE7, FireFox2.0, Opera9.01で動作確認を行っています。


コメント