JavaScript のクラスの作成 - 1 ~基本編~ [JavaScript]
◆JavaScript のクラスの作成 - 1 ~基本編~
今回のエントリーは、長いよ~www。
「プログラミング言語」の中でも、「オブジェクト指向言語」には、しっかりと定義された「役割」を与えられて、その「役割」を、最初から最後まで(エラーまで)面倒を見ることを目的とした、「オブジェクト」という概念があります。
その「オブジェクト」の概念を具現化したもののひとつが、特定の「役割」を果たすための道具の「まとまり」を一か所に集中定義できる、「クラス」の考え方です。
JavaScript が「オブジェクト指向言語」かどうかはおいといて;;;、JavaScript にも「クラス」の概念があります。
最初はちょっと「とっつきづらい」けれど、慣れちゃうと便利、便利。
ところが、慣れてくると自分のフレームワークを作ってしまうので、なかなかゼロからクラスを作る機会がなくなってくる。
こうなると、パソコンや携帯の漢字変換に慣れた人が漢字を書けなくなるかのごとく、クラスの作り方や性質を忘れちゃうんですよね。
ここでは、JavaScript のクラス周りを、備忘録代わりにメモしておきたいと思います。
ひとつのエントリーにまとめようとしたけれど、ボリューム的にムリムリ;;;。
まとめきれないので、数回に分けます。
あのねえ・・・・・・言っとくけど、「JavaScript のクラスの作成」だけで、一週間は酒飲みながら話せるからねw。
ちなみに俺は「理論派」じゃなくて、「実践主義者」です。
いくら「理論」で攻めたって、実際に使う人の手元で動かなかったら、意味ないですもんね!
・・・・・・ということで、今回は、クラスの作成の「基本編」です。
とりあえず、なんにも考えないで、ひとつクラスを作ってみます。
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'); }
ここでは、"ClassA" というクラスを作ってます。
これは「定義」だけで、このクラスを実際に使いたいときには、new で「インスタンス(実体)」を作ってやる必要があります。
こんな感じで。
var ins = new ClassA();
// クラスClassA のインスタンス ins を作成
上記の例では、変数名 ins を使って、クラス ClassA が使えるようになります。
「オブジェクト指向言語」が苦手な方は、どうもこの「定義」と「インスタンスの作成」の理解に苦しむ傾向にあるようですね。
function ClassA() は、ClassA クラスのコンストラクタ。
コンストラクタとは、クラス インスタンスを new で確保するときに、「初期化処理」として、必ず実行される処理です。
例えば、先のソースを実行すると・・・・・・
var ins = new ClassA();
どこにも明記はされていませんが、インスタンス作成時に、function ClassA() が実行されてます。
コンストラクタに指定したい関数は、<クラス名>.prototype.constructor = <関数名> で指定します。
クラス名とコンストラクタの関数名は、必ず同じにします。
そうでないと、ブラウザが理解してくれません。
また、この書式では、<関数名> のところに無名関数を使うことはできません。
これは俺の「クセ」ですが、コンストラクタの最後では、差支えがない限り、return this で、念のために自分自身への参照を呼び出し元に返すようにしています。
そうすれば、先ほどの var ins に対して、new で作ったインスタンスへの参照が、確実に返されるようになります。
普通はこんなことをしなくても、コンストラクタが自動的に自分自身へのインスタンス参照を返してくれるんですが、確か一部の古いバージョンのブラウザでは、コンストラクタで必ず this を return してやらないと、new してもインスタンス参照を受け取る変数にインスタンス参照を返してくれなかった記憶があるので、これは俺のコンストラクタ作成のときの「クセ」にしてます。
ClassA コンストラクタの中では、3つのメンバ(メソッド)を作ってます。
this.alertWord、this.alert1、this.alert3 がそれです。
これらはコンストラクタの中で作ってます。
一方、最後に prototype でプロトタイプに「動的に」追加してるメソッドがあります・・・・・・ alert2 です。
これ以降、ClassA のインスタンスを作ったら、<インスタンス名>.alertWord() メソッド、<インスタンス名>.alert1() メソッド、<インスタンス名>.alert2() メソッド、<インスタンス名>.alert3() メソッドが使えるようになります。
var ins = new ClassA();
ins.alertWord('アラート文字列'); // コンストラクタで作成
ins.alert1(); // コンストラクタで作成
ins.alert2(); // プロトタイプで作成
ins.alert3(); // コンストラクタで作成
・・・・・・こんな感じで。
一見、これらのメソッドに差はなさそうですが、実はちょっと違いがあります。
コンストラクタの外で prototype で動的にプロトタイプに乗せた alert2 と、コンストラクタの中で作ったそれ以外のメソッドは、この時点では区別がつきませんが、後々違いが出てくるんです。
インスタンスやプロトタイプでクラスから使える道具のことを、「プロパティ」と言います。
俺はあんまり JavaScript ではこの言葉を使い慣れてないので、「属性」とか書いちゃうことが多いですが、ご勘弁ください。
これらのプロパティ、一度作っても、後で削除することができます。
プロパティの削除には、delete キーワードを使います。
以下は、インスタンス プロパティ削除の例です。
// プロパティ削除のテスト クラス定義
function Class_DeleteProperty() {
this.InstanceProperty = function () { alert('InstanceProperty Exist.'); }
// インスタンス プロパティ InstanceProperty の設定
return this;
}
Class_DeleteProperty.prototype.constructor = Class_DeleteProperty;
Class_DeleteProperty.prototype.PrototypeProperty = function () { alert('PrototypeProperty Exist.'); }
// プロトタイプ プロパティ PrototypeProperty の設定
// インスタンスを作成
var ins = new Class_DeleteProperty();
// インスタンス プロパティを確認
alert('ins.InstanceProperty:\n' + ins.InstanceProperty);
ins.InstanceProperty();
// 実行してみる('InstanceProperty Exist.' と表示される)
// インスタンス プロパティを削除する
delete ins.InstanceProperty;
alert('ins.InstanceProperty:\n' + ins.InstanceProperty);
// 出力結果:undefined(削除されたので実行できない)
・・・・・・こんな感じで。
削除したプロパティは使えなくなりますが、そのクラス スコープで削除したプロパティと同名の属性の定義が隠れている場合、削除後はその「隠れていたプロパティ」が使えるようになります。
これについては、後述しましょう。
さて。
同じ名前のコンストラクタを何度も定義したら、どうなっちゃうでしょう?
さっき出てきた ClassA はいったん忘れてください。
// 1度めのコンストラクタ定義
function ClassA() { alert('before'); }
ClassA.prototype.constructor = ClassA;
// 2度めのコンストラクタ定義
function ClassA() { alert('after'); }
ClassA.prototype.constructor = ClassA;
あるいは、これ。
// コンストラクタ関数の指定
ClassA.prototype.constructor = ClassA;
// 1度めのコンストラクタ関数の定義
function ClassA() { alert('before'); }
// 2度めのコンストラクタ関数の定義
function ClassA() { alert('after'); }
関数名やコンストラクタの競合、あるいは再定義です。
この場合、コンストラクタ指定は有効なままで、競合や再定義は「より後に出てきた」方が有効です。
複数のブラウザで試しましたが、実行時エラーも出ず、動作もみな同じでした。
だから、この状態で ClassA のインスタンスを作成すると 'after' と表示されます。
同名の「より前に」書いてある定義は、「より後ろに」書いてある定義で、上書きされちゃうんですね。
これは、プロトタイプの問題ではなく、同じスコープで競合する名前の定義があると、「後勝ち」になるのが、JavaScript の普通の動作のようです。
ちなみにこのコンストラクタの指定、省略できるみたいです。
でも、個人的には、省略しないほうがいいと思うなあ・・・・・・省略すると、後で面倒になるから。
じゃ、クラス自体を再定義したら、どうなっちゃうんでしょう?
こんな感じで。
// 1度めのクラス定義
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'); }
// 2度めのクラス定義
function ClassA() {
this.alertWord = function (word) { alert(word); }
this.alert1 = function () { this.alertWord('ClassA 1 2nd'); }
this.alert3 = function () { this.alertWord('ClassA 3 2nd'); }
return this;
}
ClassA.prototype.constructor = ClassA;
ClassA.prototype.alert2 = function () { this.alertWord('ClassA 2 2nd'); }
// インスタンスを作成して実行
var ins = new ClassA();
ins.alert1();
ins.alert2();
ins.alert3();
ClassA をいったん定義し、その直後に同じ名前で ClassA を定義しなおして、メンバの中身を変えてます。
これを実行すると、結果はこうなります。
ins.alert1(); // -> 'ClassA 1 2nd'
ins.alert2(); // -> 'ClassA 2 2nd'
ins.alert3(); // -> 'ClassA 3 2nd'
前出の通り、やっぱり「後勝ち」です。
普通のプログラミング言語なら、同じスコープ(名前空間など)で、同名の「クラス」を何度も定義しようとすると、「てめえ、同じ名前を何度も定義すんじゃねーよ!!」と、怒られます。
しかし、JavaScript は、先ほどのコンストラクタの競合や多重定義と同じで、同じスコープで同じ名前を何度定義してもエラーにはならず、「より後に出てきた定義」が有効になるんですね。
これも、試してみた限りでは、どのブラウザでも同じ動作をします。
元に戻ります。
先の例では、alert2 メソッドだけをプロトタイプで、他のメソッドをコンストラクタ内でインスタンス定義しました。
じゃ、コンストラクタ内(インスタンス メンバ)とプロトタイプで、同じ名前のものを別々の動作をするように定義したら、どうなっちゃうんでしょうか(alert2 メソッドを使います)。
// クラス定義
function ClassA() {
this.alertWord = function (word) { alert(word); }
this.alert1 = function () { this.alertWord('ClassA 1'); }
this.alert2 = function () { this.alertWord('ClassA 2 Constructor'); }
// ここで alert2 を定義
this.alert3 = function () { this.alertWord('ClassA 3'); }
return this;
}
ClassA.prototype.constructor = ClassA;
ClassA.prototype.alert2 = function () { this.alertWord('ClassA 2 Prototype'); }
// プロトタイプでも alert2 を定義
// インスタンスを作成して alert2 を実行したら、どっちの alert2 が呼ばれる?
var ins = new ClassA();
ins.alert2();
この ins.alert2(); では、this.alert2() と prototype.alert2()、どっちが動作するんでしょう?
結果は、この通りです。
ins.alert2(); // -> 'ClassA 2 Constructor'
コンストラクタ内で定義した this.alert2() が実行されました。
つまり、JavaScript インスタンス メンバに対する名前の解決は、インスタンス メンバとプロトタイプの名前が重複している場合、プロトタイプよりも先に、インスタンス内の属性の定義を見に行くんです。
これも、試した中では、どのブラウザでも同じ挙動をします。
ただ、実行されないだけで、プロトタイプの alert2 も生きてます。
こんな関数を作ってみました。
// 任意のオブジェクトのすべての属性を列挙
function enumAllProperties(refObject) {
// 引数:refObject ... 属性を列挙したいクラス
// 戻値:String ...... 属性の列挙結果文字列(ひとつ1行で改行をはさむ)
var strProperties = "";
for (var item in refObject) {
strProperties = (strProperties + item + " = " + refObject[item] + "\n");
}
return strProperties;
}
// クラスのすべての prototype の列挙
function enumAllPrototypes(classType) {
// 引数:classType ... プロトタイプを列挙したいクラス
// 戻値:String ...... プロトタイプの列挙結果文字列(ひとつ1行で改行をはさむ)
return enumAllProperties(classType.prototype);
}
enumAllProperties 関数は指定したオブジェクトの属性のすべて(プロトタイプ含む)を、enumAllPrototypes 関数は引数に与えたクラスが持つプロトタイプを列挙して、文字列化して返してくる関数です。
ここで、 以下の実験をして、プロトタイプの中を見てみます。
alert(enumAllPrototypes(ClassA));
すると、こんな結果が返ってきます。
'alert2 = function () { this.alertWord('ClassA 2 Prototype'); }'
実行されることはなくても、プロトタイプで定義したalert2 メソッドは、ClassA にしっかり生き残ってるんですね。
ただし、インスタンスの持つ属性を見に行くと・・・・・・、
var ins = new ClassA();
alert(enumAllProperties(ClassA));
こんな結果が返ってきます。
'alertWord = function (word) { alert(word); }'
'alert1 = function () { this.alertWord('ClassA 1'); }'
'alert2 = function () { this.alertWord('ClassA 2 Constructor'); }'
'alert3 = function () { this.alertWord('ClassA 3'); }'
インスタンス属性とプロトタイプ、どっちも同じ名前で別々の定義ができて、かつ両方生き残っていて、けれどインスタンス上で有効なのはインスタンス属性だけ・・・・・・というのは、非常にややこしいです;;;。
・・・・・・ところが。
こういう形のクラスにした場合、インスタンスとプロトタイプで、同じ名前に対する属性が定義されています。
そのときは、名前解決の優先順序的に、インスタンス プロパティが優先的に使われます。
前述の文章の中で、プロパティを削除することができる delete について触れました。
この場合、delete を使ってインスタンス プロパティを動的に殺すと、インスタンスから見ると同じ名前のプロパティでも、その振る舞いを変えることができます。
以下は、そのサンプルです。
// alert2 を、インスタンスとプロトタイプで定義する
function ClassA_DoubleAlert2() {
this.alertWord = function(word) { alert(word); }
this.alert1 = function() { this.alertWord('ClassA_DoubleAlert2 1'); }
this.alert2 = function() { this.alertWord('ClassA_DoubleAlert2 2 Constructor'); }
// インスタンス プロパティの alert2
this.alert3 = function() { this.alertWord('ClassA_DoubleAlert2 3'); }
}
ClassA_DoubleAlert2.prototype.constructor = ClassA_DoubleAlert2;
ClassA_DoubleAlert2.prototype.alert2 = function() { this.alertWord('ClassA_DoubleAlert2 2 Prototype'); }
// プロトタイプ プロパティの alert2
// インスタンス作成
var ins = new ClassA_DoubleAlert2();
// インスタンスの alert2 実行
ins.alert2();
// -> 'ClassA_DoubleAlert2 Constructor' と表示(コンストラクタ)
//インスタンス プロパティを削除
delete ins.alert2;
// 改めて alert2 を実行
ins.alert2();
// -> 'ClassA_DoubleAlert2 2 Prototype' と表示(プロトタイプ)
このサンプルで取り上げたクラスにも、インスタンス プロパティの alert2 と、プロトタイプ プロパティの alert2 があります。
そして、両者の挙動は違います。
この場合、インスタンス プロパティが優先的に解決されるんでしたね。
じゃ、同名で定義されている alert2 のうち、インスタンス メンバを delete で消したらどうなるか・・・・・・というのが、今回の実験です。
delete によって、インスタンス プロパティである this.alert2 は、削除されました。
結果、生き残っていたプロトタイプ プロパティ側の prototype.alert2 が呼ばれるようになりました。
「名前解決」、理解できたでしょうか???
まだ続くよ~w。
当然のことながら、同じクラスのインスタンスを複数作ると、その属性の中身はインスタンスごとに個別に持たれます。
それは、以下の実験を見ても、明らかです。
// クラス定義
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'); }
// インスタンス ins1 を作り、alert1 メンバを再定義
var ins1 = new ClassA;
ins1.alert1 = function() { this.alertWord('ClassA 1 ins1'); }
// インスタンス ins2 を作り、alert1 メンバを再定義
var ins2 = new ClassA;
ins2.alert1 = function() { this.alertWord('ClassA 1 ins2'); }
// インスタンス ins3 を作り、alert1 メンバを再定義
var ins3 = new ClassA;
ins3.alert1 = function() { this.alertWord('ClassA 1 ins3'); }
// 各インスタンスの alert1 を実行
ins1.alert1(); // -> 'ClassA 1 ins1'
ins2.alert1(); // -> 'ClassA 1 ins2'
ins3.alert1(); // -> 'ClassA 1 ins3'
そりゃあそうですよね、そうでなけりゃインスタンスを別々に作る意味がありません。
つまり、インスタンスの属性は「参照」ではなく、「値」だということです。
じゃ、プロトタイプはどうなの?
以下の実験を見ると、その仕組みがわかります。
// クラス定義
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'); }
var alert2Store = ClassA.prototype.alert2;
// 保存する(これポイント、alert2 = function () { this.alertWord('ClassA 2'); } を保存する)
// プロトタイプ alert2 を再定義して、すぐにインスタンス ins1 を作成
ClassA.prototype.alert2 = function() { alert('ClassA 2-1'); }
var ins1 = new ClassA;
// プロトタイプ alert2 を再定義して、すぐにインスタンス ins2 を作成
ClassA.prototype.alert2 = function() { alert('ClassA 2-2'); }
var ins2 = new ClassA;
// プロトタイプ alert2 を再定義して、すぐにインスタンス ins3 を作成
ClassA.prototype.alert2 = function() { alert('ClassA 2-3'); }
var ins3 = new ClassA;
ClassA.prototype.alert2 = alert2Store;
// 元に戻す(これポイント、最後の prototype.alert2 の定義で、最初に保存しておいた alert2 = function () { this.alertWord('ClassA 2'); } に戻す)
// 各インスタンスの alert2 を実行
ins1.alert2(); // -> 'ClassA 2'
ins2.alert2(); // -> 'ClassA 2'
ins3.alert2(); // -> 'ClassA 2'
プロトタイプで指定した属性の実体は、インスタンスごとに個別に持たれることはありません。
もし、インスタンス生成時に、インスタンスが個別にプロトタイプの実体を継承するとするならば、最後の3行では、このような結果になるはずです。
// 各インスタンスの alert2 を実行
ins1.alert2(); // -> 'ClassA 2-1'
ins2.alert2(); // -> 'ClassA 2-2'
ins3.alert2(); // -> 'ClassA 2-3'
ひとつのクラスに対して、そのクラス インスタンスすべてに対して共通の処理を保証するのが、プロトタイプなんです。
まだまだ続くよ~www。
じゃあ、ひとつのインスタンスを作成して、プロトタイプ定義を変えながら、そのプロトタイプ メソッドの定義を動的に変化させていくと、どういう挙動をするのか?
以下が、その実験結果です。
// クラス定義
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'); }
// 唯一のインスタンス作成
var alert2Store = ClassA.prototype.alert2; // 保存する
var ins = new ClassA;
// プロトタイプ alert2 を再定義して、先ほど作成したインスタンス ins の alert2 を実行
ClassA.prototype.alert2 = function() { alert('ClassA 2-1'); }
ins.alert2(); // -> 'ClassA 2-1'
// プロトタイプ alert2 を再定義して、先ほど作成したインスタンス ins の alert2 を実行
ClassA.prototype.alert2 = function() { alert('ClassA 2-2'); }
ins.alert2(); // -> 'ClassA 2-2'
// プロトタイプ alert2 を再定義して、先ほど作成したインスタンス ins の alert2 を実行
ClassA.prototype.alert2 = function() { alert('ClassA 2-3'); }
ins.alert2(); // -> 'ClassA 2-3'
ClassA.prototype.alert2 = alert2Store; // 元に戻す
インスタンスにはまったく手を加えてないのに、プロトタイプ定義を変えていくだけで、インスタンス メソッドの動きがどんどん変わるんですね。
これは、インスタンスがプロトタイプを呼び出すときに、プロトタイプを「参照」で解決していることを意味します。
じゃあさ。
プロトタイプを、インスタンスの属性に指定したら、どうなるの???
インスタンスの属性は「値」だけど、プロトタイプは「参照」。
どっちが勝つんでしょう?
以下が、その実験結果です。
// クラス定義
function ClassA_CallPrototypeAlert2() {
this.alertWord = function(word) { alert(word); }
this.alert1 = function() { this.alertWord('alert1'); }
this.alert2 = ClassA_CallPrototypeAlert2.prototype.alert2;
// alert2 にはプロトタイプ呼び出しを使う
this.alert3 = function() { this.alertWord('alert3'); }
}
ClassA_CallPrototypeAlert2.prototype.constructor = ClassA_CallPrototypeAlert2;
// 1度めのプロトタイプ alert2 定義
ClassA_CallPrototypeAlert2.prototype.alert2 = function() { this.alertWord('alert2 Prototype Before'); }
// 唯一のインスタンス作成
var ins = new ClassA_CallPrototypeAlert2();
ins.alert2(); // -> 'Before'
// 2度めのプロトタイプ alert2 定義
ClassA_CallPrototypeAlert2.prototype.alert2 = function() { this.alertWord('alert2 Prototype After'); }
ins.alert2(); // -> 'Before' のまま
前出の enumAllPrototypes 関数で確認してみると、確かに2度めの alert2 のプロトタイプ定義で、alert2 の内容は変わっています。
しかし、その後に実行した ins.alert2(); では、1度めのプロトタイプ定義の結果を返してきます。
これは、インスタンス メンバの alert2 をコンストラクタで定義したとき、その時点でのプロトタイプ alert2 の定義を、そっくりそのまま「値」としてコピーしてきていることを示します。
「参照」を持つわけじゃないんですね。
インスタンス メンバは、やっぱり「値」だった・・・・・・ということです。
まだ続きがあるんだよ~!!
じゃ、いったい、コンストラクタは、インスタンスとプロトタイプの、どっちにいるんでしょう!?
その謎を解明するために、以下のようなコードを書きました。
// クラス定義
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'); }
// インスタンスを作成して状況を確認
var ins = new ClassA();
alert('ClassA prototype:\n' + ClassA.prototype.constructor);
alert('ClassA:\n' + ClassA.constructor);
alert('instance:\n' + ins.constructor);
出力は以下の通りです。
ClassA prototype:
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:
function { [ native code] };
instance:
function ClassA() {
this.alertWord = function (word) {alert(word);};
this.alert1 = function () {this.alertWord("ClassA 1");};
this.alert3 = function () {this.alertWord("ClassA 3");};
return this;
}
結論。
プロトタイプとインスタンス、どっちにもコンストラクタは存在しますwww。
ここからは憶測になりますが、インスタンスの確保前に、すでにインスタンス メンバにコンストラクタが設定されていることは考えづらいので、おそらくインスタンス確保時にはプロトタイプのコンストラクタが起動されるか、インスタンスが確保されてからプロトタイプが保持しているコンストラクタがインスタンスに設定されたうえで起動されるか、のどちらかではないかと思います。
しかし、コンストラクタの扱いは、充分気をつけないといけませんね。
やりようによっては、再帰的なコンストラクタや、複数のメソッドを使っての循環参照ができちゃうでしょうから・・・・・・。
ちょっと気になったのは、ClassA.constructor で出てきた [native code] です。
[native code] とは、ネイティブ コード、すなわち・・・・・・なんと言ったらいいのかなあ、アセンブラとかをご経験の方なら、すぐにお分かりになると思うんですが;;;。
そのマシンでしか理解できない、「機械語」の世界です。
まあ、でも、クラスの(プロトタイプでもインスタンスでもない)コンストラクタを直接いじったり叩いたりするというシチュエーションはあんまり考えつかないので、ここでは深追いしないでおきます。
でも、ちょっと待って!
ここで、いったん整理してみましょう。
- コンストラクタは、クラス定義の時に、クラスのプロトタイプで定義する。
- インスタンスを作成するときの初期化は、コンストラクタで処理される。
- プロトタイプは、そのクラスのインスタンスから「参照」され、すべてのインスタンスに共通の処理を保証する。
- クラスのインスタンスを作ってみると、クラスのプロトタイプにも、インスタンスにも、コンストラクタが存在する。
- インスタンスの属性は、「値」である。
- JavaScript では、同じスコープの同じ名前の定義を何度も実装でき、結果は「後勝ち」。
おかしくないですか?
だとすると、ですよ。
いったんクラス定義して、インスタンスを作った後に、そのクラスのコンストラクタを再定義したら、1度めのクラス定義で作ったインスタンスは、どうなっちゃうんでしょう???
だって、普通に考えれば、インスタンスを作った時点で、必ずコンストラクタを通ってるわけだから。
だけど、プロトタイプはいつでも設定できるわけでしょ?
だから、クラスのコンストラクタを1度定義して、インスタンスを作って(=その時点のコンストラクタを通して)、その後でそのクラスのコンストラクタを違う内容で再定義したら、再定義の前に作られたはずのインスタンスは、どうなるの???
・・・・・・ということで、こんな感じで実験です。
// 1度めのクラス(コンストラクタ)定義
function ClassA_Reconstruct() { this.alert1 = function () { alert('before'); } }
ClassA_Reconstruct.prototype.constructor = ClassA_Reconstruct;
// この時点のクラス インスタンスを作成
var ins1 = new ClassA_Reconstruct();
// 2度めのクラス(コンストラクタ)定義
function ClassA_Reconstruct() { this.alert1 = function () { alert('after'); } }
ClassA_Reconstruct.prototype.constructor = ClassA_Reconstruct;
// この時点のクラス インスタンスを作成
var ins2 = new ClassA_Reconstruct();
// クラス再定義前に作ったインスタンスの alert1 実行
ins1.alert1(); // ←こいつはどうなる?
// クラス再定義後に作ったインスタンスの alert1 実行
ins2.alert1(); // ←こいつは 'after' に決まってる
・・・・・・どうなったと思います?
結果は、ブラウザ依存でした!
// クラス再定義前に作ったインスタンスの alert1 実行
ins1.alert1();
// Firefox のみ 'before', 他のブラウザは 'after'
また出たよ・・・・・・ブラウザ依存;;;。
「他のブラウザ」とは、例によって Internet Explorer、Opera、Safari、Google Chrome です。
インスタンスのコンストラクタ(ins.constructor)の中身も確認してみましたが、上記を裏付ける結果となりました。
どういうこと???
ということで、1度めのクラスのコンストラクタ定義に alert を挟んで、1度めのクラス定義のコンストラクタが起動しているかどうかを確認しました。
// 1度めのクラス(コンストラクタ)定義
function ClassA_Reconstruct() {
alert('Execute ClassA_Reconstruct constructor At: ' + this);
// コンストラクタが起動すれば、この alert が表示されるはず
this.alert1 = function () { alert('before'); }
}
ClassA_Reconstruct.prototype.constructor = ClassA_Reconstruct;
結果、この alert('Execute ClassA_Reconstruct constructor At: ' + this); が起動したのは、Firefox だけでした。
他のブラウザでは、1度めのインスタンス ins1 を作成するときに、1度めの ClassA_Reconstruct クラス定義のコンストラクタを通っていないことになります。
つまり、こういう結論です。
- Firefox は、クラス インスタンスが作成されるとき、プロトタイプからインスタンスの constructor メンバに、その時点のコンストラクタの「値のコピー」を設定する。
- Firefox 以外のブラウザ(Internet Explorer、Opera、Safari、Google Chrome)は、クラス定義と、インスタンスを使ったスクリプトの実行パスが分かれている。先に行われるのはクラス定義のパス。各々のパスの中では、「後勝ち」が成立している(たぶんw)。
こんなことになっているとは;;;。
ECMAScript のまっとうな仕様については知らないんですが、Firefox の動作の方が、正常な気がします・・・・・・。
クラスのコンストラクタを動的に制御する場合、ブラウザ依存に気をつけろ!ってことですね。
まだまだまだ続くよ~!!!
いままでの「実証主義」的な実験で、以下の事実が判明しました。
- 他言語では普通「怒られる」再定義は、JavaScript においては許容され、ルールは「後勝ち」。
- プロトタイプは、クラス単位で共有される。しかも動的に変化させられる。
- インスタンスは、当然ながらインスタンスごとに保持される。
ということで、おぼろげながら、「クラス定義」についてはプロトタイプが効果的らしく、「インスタンス管理」については "this" キーワードを使ったメンバ管理が効果的らしいことがわかってきました。
ただし!
これは、単独のクラスの話であって、今後エントリー予定の「クラスの継承」になると、また話がややこしくなってきますよw。
いや~、最後までお付き合い、ありがとうございました!
今回のエントリーは、ひとまずこれで終わりです。
これらの実験は、あくまで「個人的メモ」ではありますが、少しでもみなさまのお役に立てれば、なおうれしいです。
ところが、クラス定義は、まだ数種類あります!!
次回は、「クラスの作成 - 2 ~プロトタイプ編~」を予定しています。
・・・ゴメンナサイ、長すぎて読みきれません。 また、出直してきます、今日は予習したフリで<title>ecc@授業中<title> /a /br /body /html /出直してきますw
俺の異国Languageも実践はです
アカデミックに異国語を習得している人は学問、俺の異国語は自ミッションのTool・・・人の家で書いちゃいけませんね、自分のところで書かないと、失礼しました、先生
2時間目のダイヘンは、頼んであります。です、
by One-for-you (2010-12-21 22:39)
すみません、読んでくれてる方にはめっちゃ迷惑な、長~いエントリーでした;;;。
by みみちゃん (2010-12-22 20:25)
いや、ちがうと思う
これが、だんだんとカテゴライズされてけば、きっと、俺も雰囲気から書くほうに変われると、「意気込み」をありがとうございますw
by One-for-you (2010-12-28 01:16)
いえいえ、こちらこそ!
どうもありがとうございます!
そんなお言葉を頂けて、また「やる気」が「ふつふつ」と沸いてきました♪
by みみちゃん (2010-12-28 15:48)