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で動作確認を行っています。