JavaScript のクラスの作成 - 3 ~クロージャ編~ [JavaScript]
◆JavaScript のクラスの作成 - 3 ~クロージャ編~
前回のエントリー「JavaScript のクラスの作成 - 2 ~プロトタイプ編~」の続きです。
今回は、JavaScript の「クラス」の作成の中でも、「プロトタイプが呼べないクラス」に注目します。
クラスの作り方によっては、「プロトタイプが定義されているのに、インスタンスから呼び出せない」という状況が発生します。
それは、そうなってしまうのを了解したうえで、それでも得られるメリットを活かすクラス作りを、わざとやるんです。
今回は、これを覚書にしておきます。
まずは、前回・前々回のエントリーの「クラス定義」を、改めて提示しておきます。
function ClassA() {
this.alertWord = function (word) { alert(word); }
this.alert1 = function () { this.alertWord('ClassA 1'); }
this.alert3 = function () { this.alertWord('ClassA 3'); }
return this;
}
ClassA.prototype.constructor = ClassA;
ClassA.prototype.alert2 = function () { this.alertWord('ClassA 2'); }
今回のエントリーでも、これをいろいろと「こねくりまわし」ます。
さて。
コンストラクタの作り方の基本は、前々回のエントリーで書きました。
コンストラクタは、特に差支えがない限り、自分自身への参照を返します。
しかし、やろうと思えば、こんなクラスも作れちゃいます。
// クラス定義
function TestClass() { return function() {}; } // 無名関数を返す
TestClass.prototype.constructor = TestClass;
TestClass.prototype.testPrototype = "This Class Instance has Valid Prototype."
// プロトタイプ プロパティ testPrototype を作成
上記の TestClass クラスでは、プロトタイプで testPrototype プロパティを作ってます。
本来なら、この testPrototype プロパティは、インスタンスから呼べなきゃいけません。
でなきゃ、作った意味がないですもんね。
でも、インスタンスを作っても、testPrototype プロパティは呼べません。
// インスタンス作成
var ins = new TestClass();
alert(ins.testPrototype); // -> undefined、つまり未解決
その理由は・・・・・・もう気づいた方も、きっといますよね。
「コンストラクタで this 以外を return してるんだから、あたりまえじゃん!」
「新規作成した TestClass クラス インスタンスへの参照が解決してないだけでしょ?」
・・・・・・そうです、その通りです。
コンストラクタが無名関数を返すからです。
コンストラクタを実行すると、返してくるのは、(new で新しく作成された、「どこか」に存在する)TestClass クラス インスタンス内の無名関数への参照です。
// クラス定義
function TestClass() { return function() {}; } // 無名関数を返す
だから、TestClass のコンストラクタを起動してインスタンスを確保しても、それは作成した TestClass クラス インスタンス内にある function() {}; への参照であって、 TestClass クラス インスタンスではありません。
// インスタンス作成
var ins = new TestClass();
// ins に返されるのは TestClass クラス インスタンスではない
// 新しく作成された TestClass クラス インスタンス内の無名関数への参照
TestClass コンストラクタで作成したインスタンスから testPrototype プロトタイプが呼べない理由は、上記のとおりで、TestClass コンストラクタから返されるインスタンスが TestClass クラス型じゃないからです。
「そんなクラス定義して、意味あるの?」
・・・・・・やるんです、ときどき;;;。
それも、わざと。
他の高級言語では、「識別子」という考え方があります。
「このクラスのこのメンバは、外から一切触れないようにしよう(Private)」とか。
「このクラスのこのメンバは、外から自由にアクセスできるようにしよう(Public)」とか。
JavaScript では、この「識別子」の考え方がありません。
そこで、頭のいい人が考え出したのが、JavaScript でこの「識別子」の考え方を疑似的に実現する「クロージャ」という考え方です。
コンストラクタから返すインスタンスを、クラス インスタンス内の一部に限定して、コンストラクタから返されるインスタンスに含まれないプロパティは、外から自由にアクセスできないようにしよう(Closure)、というものです。
以下は、前出の ClassA とほぼ同じ動作をする、クロージャ版 ClassA ともいうべき、ClassA_Closure クラスです。
// クラス定義
function ClassA_Closure() {
// この alertWord には、外からアクセスできない
var alertWord = function(word) { alert(word); };
// コンストラクタが返すのは、このインスタンス
return {
alert1:
function() { alertWord('ClassA 1'); },
alert2:
function() { alertWord('ClassA 2'); },
alert3:
function() { alertWord('ClassA 3'); }
};
}
ClassA_Closure.prototype.constructor = ClassA_Closure;
// 一応プロトタイプを付けておく
ClassA_Closure.prototype.test1 = function() { alert('test1'); }
ClassA_Closure.prototype.test2 = function() { alert('test2'); }
先に提示した ClassA クラスと、ここで定義した ClassA_Closure クラスは、単独のクラスとしてみれば、動作はあまり変わりません。
ただ、ClassA_Closure クラスのコンストラクタで取得したインスタンスでは、alertWord メンバを呼ぶことができません。
インスタンスを作ると・・・・・・、
// インスタンス作成
var ins = new ClassA_Closure();
この ins インスタンスは、クラス定義の以下の赤太字の部分のみのインスタンスです。
// クラス定義
function ClassA_Closure() {
// この alertWord には、外からアクセスできない
var alertWord = function(word) { alert(word); };
// コンストラクタが返すのは、このインスタンス
return {
alert1:
function() { alertWord('ClassA 1'); },
alert2:
function() { alertWord('ClassA 2'); },
alert3:
function() { alertWord('ClassA 3'); }
};
}
ClassA_Closure.prototype.constructor = ClassA_Closure;
// 一応プロトタイプを付けておく
ClassA_Closure.prototype.test1 = function() { alert('test1'); }
ClassA_Closure.prototype.test2 = function() { alert('test2'); }
しかし、ClassA_Closure インスタンスは、確かに新しく確保され、参照が取得できないだけで、どこかにいるんです。
ins で取得したインスタンスは、その「どこかにいる」 ClassA_Closure クラス インスタンスの中の return 部分です。
だから、ins の参照が生きている限り、どこかにいる ClassA_Closure インスタンス がメモリ上から消失することはありません。
しかも、ins が参照しているのは、そのどこかにいる ClassA_Closure インスタンス のローカル属性ですし、ins 内のプロパティで参照している alert1()、alert2()、alert3() 関数が呼び出している alertWord も、同じ ClassA_Closure インスタンス 内のローカル属性 alertWord ですから、alert1()、alert2()、alert3() 関数から、同じ ClassA_Closure インスタンス スコープ内の alertWord ローカル属性が呼び出せる、というわけです!
ただし、プロトタイプ プロパティはその(どこかにいる) ClassA_Closure に対して定義されていて、ins はその ClassA_Closure に対する参照ではありませんから、ins インスタンスは ClassA_Closure のプロトタイプ プロパティを呼び出せないわけです。
だから、この ins インスタンスで呼び出されるのは、return 内で定義される、以下のインスタンス プロパティだけです。
// インスタンス作成
var ins = new ClassA_Closure();
// ins.alwrWord(); // -> undefined 未解決で呼べない
ins.aler1(); // 実行できる
ins.aler2(); // 実行できる
ins.aler3(); // 実行できる
// ins.test1(); // -> undefined 未解決で呼べない
// ins.test2(); // -> undefined 未解決で呼べない
ちなみに、前々回のエントリーで掲載した enumAllProperties 関数を呼ぶとわかるんですが、alert1()関数・alert2()関数・alert3()関数 は、ins インスタンスのインスタンス プロパティです。
また、前々回のエントリーで掲載した enumAllPrototypes 関数を呼ぶとわかるんですが、test1 プロトタイプ プロパティ・test1 プロトタイプ プロパティ は、しっかり ClassA_Closure クラスのプロパティとして、定義が生きていますです。
すごいよね、これを考えた人;;;。
こういうクラスの実装方がどういう役割を果たすかですが、その典型例はカウンターあたりではないかと思います。
以下に、カウンター クロージャ クラス TestCounter の例を挙げておきます。
// クラス定義
function TestCounter() {
// この counter には、外からアクセスできない
var counter = 0;
// コンストラクタが返すのは、このインスタンス
return {
// カウンタ値の取得(インスタンス メンバ)
get:
function() { return counter; },
// カウンタのインクリメント(インスタンス メンバ)
inc:
function() { counter++; return counter; },
// カウンタのデクリメント(インスタンス メンバ)
dec:
function() { counter--; return counter; },
// カウンタのリセット(インスタンス メンバ)
reset:
function() { counter = 0; return counter; }
};
}
TestCounter.prototype.constructor = TestCounter;
このインスタンスを作成すると、アクセス スコープは以下のようになります。
// インスタンス作成
var ins = new TestCounter();
var currentCounter;
// ins.counter = 0; // -> undefined 未解決で呼べない
// currentCounter = ins.counter; // -> undefined 未解決で呼べない
currentCounter = ins.get(); // 実行できる、現在のカウンタ値を取得する
currentCounter = ins.inc(); // 実行できる、カウンタ値を1増やす
currentCounter = ins.dec(); // 実行できる、カウンタ値を1減らす
currentCounter = ins.reset(); // 実行できる、カウンタ値をゼロクリアする
こうすると、counter ローカル変数は外部から一切アクセスできず、ins.get() 関数、ins.inc() 関数、inc.dec 関数()、inc.reset() 関数でしかアクセスできなくなります。
counter 変数の「堅牢性」が、高くなるわけです。
クロージャは、プロトタイプ アクセスを捨てて、この「堅牢性」を担保するための、クラスの作成テクニックなんですね。
とりあえずここまでで、「クラスの作成」の「メモ書き」は、いったん終わりです。
あくまで「個人的メモ」ですが、お役にたてれば幸いです。
さあ!
次はいよいよ、「クラスの継承」に行きましょうか!
また、ややこしいよ~!!
・・・・・・ということで、次回は「クラスの継承」いきましょう。
コメント 0