不思議の国のJavaScript
オブジェクト指向に関するtips
《 初回公開:だいぶ昔で覚えていない , 最終更新:未 》
本稿は私もあまり理解しきれていない点について、自分の考えを整理するために書いているものなので他の記事もそうだが特に誤った事を書いている可能性が高いので注意していただきたい。
JavaScript
には
クラス
という正式の概念がない。
Java
,
C#
等の通常のプログラミング言語における
クラスベースのオブジェクト指向と呼ばれているが
、JavaScriptは
プロトタイプベースの
オブジェクト指向と呼ばれ変わった方法で実現されている。
クラスの定義
クラスFooを定義して、インスタンス化して変数fooに代入して操作する例を示します。
リスト1
04 | function Foo(init_inst_member) { |
06 | this .inst_member = init_inst_member; |
10 | Foo.cls_member = "クラス変数" ; |
12 | Foo.cls_method = function () { |
17 | Foo.prototype.inst_method = function () { |
18 | alert( "インスタンスメソッドの実行" ) |
22 | Foo.prototype.inst_method2 = function () { |
23 | alert( "二つ目インスタンスメソッドの実行" ) |
27 | Foo.prototype.inst_member_proto = "prototypeオブジェクトのプロパティ" ; |
31 | var foo = new Foo( "インスタンス変数の初期値" ) |
41 | alert(foo.inst_member_proto); |
44 | foo.inst_method_ex = function () { |
クラスに付随するメンバーを定義するには、以下3種類のプロパティに変数や関数を代入する事で実現されている。
これらのどのプロパティに変数や関数を代入するかによってクラスのメンバーとして機能したりインスタンスのメンバーとなったりする。
-
コンストラクタである関数オブジェクト(この例ではFoo関数)のプロパティに変数や関数を代入する。
クラス変数やクラスメソッドとなる。
-
コンストラクタである関数オブジェクトのprototypeプロパティのプロパティに変数や関数を代入する。
クラスのインスタンスで共通に使える変数や関数(メソッド)となる。
-
インスタンス変数のプロパティに対して直接、変数や関数を代入する。
そのインスタンスだけで使える固有の変数や関数(メソッド)となる。
コンストラクタの中でthisのプロパティとして定義された変数や関数は基本的にはインスタンスメンバーとなるが、コンストラクタの外でインスタンス変数のプロパティとして定義された変数や関数は
Ruby
で言うところの特異メンバーに相当すると考えて良いと思う。
クラスのメンバーはすべてpublicとなりメンバーへのアクセス権の設定(privateやprotect)はできない。
prototypeオブジェクトのプロパティは特異なふるまいをするのだが、私ごときが下手な説明を加えてもかえって混乱を招くだけなので理解されているものとして次に進む。
applyメソッドとcallメソッド
上記のthisの例で関数の中のthisは通常はwindowオブジェクトを指している事が多いが、これを意識的に別のオブジェクトを指すように指定できる。
これにはFunctionオブジェクトのapplyメソッドかcallメソッドを使う。
applyメソッドとcallメソッドの違いはapplyメソッドが関数への引数を配列として渡す事です。
前項のサンプルプログラムの関数fooを以下のようにthisにHogiクラスのオブジェクトを指定して呼び出す。
リスト2
applyメソッドとcallメソッドを引数を指定して呼び出す。
リスト3
02 | alert( "this.prop=" + this .prop+ "\n" |
09 | this .prop= "Hogi.prop" ; |
13 | foo.apply(obj,[ "arg_a" , "arg_b" , "arg_c" ]); |
14 | foo.call(obj, "arg_a" , "arg_b" , "arg_c" ); |
CallオブジェクトとArgumentsオブジェクトとcalleeプロパティ
関数が呼び出された時に、Callオブジェクトという見えない(プログラムからアクセスする事ができない)オブジェクトが作成される。
このCallオブジェクトはactivationオブジェクトとも呼ばれる。
Callオブジェクトは関数のスコープチェインの先頭になる。
関数の内部(ローカルスコープ)がCallオブジェクトとなるため、関数の内部でvarキーワードで定義された変数や関数定義時の名前付き引数はCallオブジェクトのプロパティとなる。
Callオブジェクトの次のスコープチェーンは関数定義の外側になる。この事を利用してネストされた関数を使って
クロージャ
の機能を実現できる。
関数呼び出し時に指定された引数はCallオブジェクトのargumentsプロパティにArgumentsオブジェクトという配列のようなオブジェクトに格納される。 このArgumentsオブジェクトを利用する事で名前付き引数でアクセスする事ができない可変長の引数を利用する事ができる。
Argumentsオブジェクトにはcalleeプロパティという呼び出された関数自体を参照するプロパティが存在する。
これを利用すると以下のように無名関数でも再帰を実現できる。
リスト4
5 | return arguments.callee(n - 1) * n; |
8 | alert( "10の階乗は" +func(10)); |
クラスの継承とミックスイン
前述のクラスFooを継承したChildFooクラスを実装したコードを以下に示す。
リスト5
04 | function ChildFoo(init_inst_member) { |
06 | Foo.call( this , init_inst_member); |
10 | ChildFoo.prototype = new Foo(); |
15 | delete ChildFoo.prototype.inst_member; |
19 | ChildFoo.prototype.constructor = ChildFoo; |
22 | ChildFoo.prototype.inst_method2 = function () { |
25 | Foo.prototype.inst_method2.call(); |
27 | alert( "親クラスのインスタンスメソッドのオーバライト" ) |
32 | var childfoo = new ChildFoo( "子クラスのインスタンス変数の初期値" ); |
35 | alert(childfoo.inst_member); |
38 | childfoo.inst_method(); |
41 | childfoo.inst_method2(); |
言語レベルで継承をサポートしていないので通常のプログラミング言語より複雑になってしまう。
これがプロトタイプベースのオブジェクト指向と呼ばれるゆえんである。
また、別のクラスのメンバーをコピーする事によって以下のようにクラスの継承の機能を擬似的に実現する事もできる。
これはRubyの多重継承を実現するための仕組みであるモジュールのインクルードに相当するものでRubyと同様
ミックスイン
と呼ばれる。
これにより多重継承に似た機能が実現できる。
RubyのModuleクラスのincludeメソッドをまねてミックスインの実装をこころみたのが次の例です。
リスト6
08 | function incledeClass(childClass, superClass, isConstructor, args){ |
09 | var superPrototype=superClass.prototype; |
10 | for ( var p in superPrototype){ |
11 | if ( typeof (superPrototype[p])== "function" ){ |
12 | childClass[p]=superPrototype[p]; |
18 | superClass.apply(childClass, args); |
22 | function MixindFoo(init_inst_member) { |
24 | incledeClass( this , Foo, true , arguments); |
29 | var mixindFoo = new MixindFoo( "ミックスインクラスのインスタンス変数の初期値" ) |
32 | alert(mixindFoo.inst_member); |
35 | mixindFoo.inst_method(); |
変数のいろいろ
JavaScriptではいろいろな種類の変数を使う事ができるので、混乱する事がある。
以下の例のように同じ変数名aの変数でもさまざま状況で使い分ける事はできる。
リスト7
04 | < meta http-equiv = "Content-Type" content = "text/html; charset=windows-31j" > |
05 | < title >変数宣言のいろいろ</ title > |
08 | < div id = "message" ></ div > |
09 | < script type = "text/javascript" > |
11 | function addMessage(message) { |
12 | document.getElementById("message").innerHTML += message + "< br />"; |
15 | // グルーバル変数a (Windowオブジェクトのプロパティa) |
19 | Foo.prototype.a = "Fooクラスのprototypeオブジェクトのプロパティa"; |
23 | addMessage("var a=" + a); |
24 | addMessage("this.a=" + this.a); |
25 | addMessage("window.a=" + window.a); |
30 | bar.a = "関数barのプロパティa"; |
35 | addMessage("Foo.a=" + Foo.a); |
36 | addMessage("Foo.prototype.a=" + Foo.prototype.a); |
37 | addMessage("foo.a=" + foo.a); |
39 | addMessage("bar.a=" + bar.a); |
実行結果
var a=ローカル変数
this.a=インスタンス変数
window.a=グローバル変数
a=グローバル変数
Foo.a=クラス変数
Foo.prototype.a=Fooクラスのprototypeオブジェクトのプロパティa
foo.a=インスタンス変数
bar.a=関数barのプロパティa
上記のように関数の中の「var a;」と「this.a」は別の変数として認識される。
グローバル変数はwindowオブジェクトのプロパティとなる。
thisについて
JavaScriptのthisの指し示す値は実行コンテキストによって異なる。
トップレベルや通常の関数の中ではwindowオブジェクトを指す。
alertメソッドなどがオブジェクトを指定しないで使えるのはwindowオブジェクトのメソッドだから。
frameタグがある場合はwindowオブジェクトはフレームごとに存在する。
当然クラスのコンストラクタの中ではクラスのインスタンスを示している。
面白いのはクラスのコンストラクタの中でネスト関数を記述するとその中でもwindowオブジェクトを指している事です。
また、ボタンのイベント関数の中では、イベント対象のボタンを指している。
以下のコードは、divタグに各実行コンテキストごとのthis.nameプロパティの値を表示した例です。
リスト8
02 | < form name = "form.name" onsubmit = "return false;" > |
04 | < button name = "button.name" onclick = "addMessage('button: ' + this.name);" > |
08 | < div id = "message" ></ div > |
09 | < script type = "text/javascript" > |
11 | function addMessage(message) { |
12 | document.getElementById("message").innerHTML += message + "< br />"; |
15 | var name = "グルーバル変数のname(window.name)"; |
17 | // トップレベルのthisが何を指しているか調べる |
18 | addMessage("1: " + this.name) |
19 | // windowオブジェクトがthisを指している事を確認する |
20 | addMessage("2: " + window.name) |
22 | // 関数の中のthisが何を指している事を確認する |
24 | var name = "foo関数中のローカル変数" |
25 | addMessage("3: " + this.name) |
30 | this.name = "hogi.name"; |
32 | // コンストラクタの中のネスト関数のthisを確認 |
33 | addMessage("4: " + this.name) |
37 | addMessage("5: " + this.name) |
39 | var hogi = new Hogi(); |
実行結果
1: グルーバル変数のname(window.name)
2: グルーバル変数のname(window.name)
3: グルーバル変数のname(window.name)
4: グルーバル変数のname(window.name)
5: hogi.name
クラスの判別と
prototypeオブジェクトのconstructorプロパティとinstanceofとtypeof演算子
typeof 演算子を使用すると型情報を示す文字列を取得する事ができますが、取得できる文字列は、"number"、"string"、"boolean"、"object"、"function"、"undefined"
のどれかになります。
例えば、「クラスの定義」の項のサンプルリストの変数fooに対してtypeof演算子を使うとかえされる文字列は”Foo”ではなく"object"になります。
リスト9
1 | alert( "typeof foo => " + typeof foo); |
クラスのインスタンスかどうかを判断するためにinstanceof演算子を使う事ができます。
リスト10
1 | alert( "foo instanceof Foo => " + (foo instanceof Foo)); |
また、instanceof演算子はプロトタイプチェンを使ったクラスの継承関係にあるクラスに対してもtrueを返します。
しかし、プロトタイプチェンを使わないミックスインした親クラスに対してはfalseになってしまいます。
以下に例を示します。
リスト11
02 | alert( "foo instanceof Object => " + (foo instanceof Object)); |
05 | alert( "childfoo instanceof ChildFoo => " + (childfoo instanceof ChildFoo)); |
07 | alert( "childfoo instanceof Foo => " + (childfoo instanceof Foo)); |
09 | alert( "childfoo instanceof Object => " + (childfoo instanceof Object)); |
12 | alert( "mixindFoo instanceof MixindFoo => " + (mixindFoo instanceof MixindFoo)); |
14 | alert( "mixindFoo instanceof Foo => " + (mixindFoo instanceof Foo)); |
16 | alert( "mixindFoo instanceof Object => " + (mixindFoo instanceof Object)); |
クラスのprototype.constructorプロパティにはコンストラクタ関数が自動的に代入されています。
従って、継承関係を含まずに直接にどのクラスかを判断するにはconstructorプロパティが使えます。
リスト12
2 | alert( "foo.constructor==Foo => " +(foo.constructor==Foo)); |
4 | alert( " foo.constructor==Object => " + (foo.constructor==Object)); |
但し、クラスの継承の例で示したようにconstructorプロパティは代入可能なため、作為的にconstructorプロパティに別の値を代入した場合にはこの関係は当然なりたたなくなります。
名前空間の利用
他のJavaScriptライブラリと名前の衝突が起こらないように
名前空間
を定義する必要があります。
名前空間はJavaScriptの言語仕様では定義されていません。
しかしJavaScriptのオブジェクトとプロパティの関係を利用すれば擬似的に名前空間を実現できます。
gudonとい名前空間のhogeいう関数を定義してこれを呼び出すには以下のようにします。
リスト13
06 | else if ( typeof gudon != "objec" ) |
07 | throw new Error( "名前空間名gudonは既に使われています!!" ) |
10 | gudon.hoge = function () { |
12 | alert( "call gudon.hoge" ); |
このコードはgudonという名前のクローバル変数に空のオブジェクトを代入して新たにオブジェクトのプロパティhogeを定義して関数を代入しているのと同じです。
ここでは名前空間に関数を定義していますが、もちろんクラスや変数も定義できる。
名前空間をネストしてより名前の衝突の可能性を少なくする事ができます。
以下の例では名前空間gudonの下にさらにbarという名前空間を定義しています。
リスト14
06 | else if ( typeof gudon.bar != "objec" ) |
07 | throw new Error( "名前空間名gudon.barは既に使われています!!" ) |
10 | gudon.bar.hoge = function () { |
12 | alert( "call gudon.bar.hoge" ); |
ネストした名前空間を使用すると名前が長くなってわずらわしい場合は別の変数に代入する事でこれを解決できる。
リスト15
2 | var func = gudon.baz.hoge; |
最後に
JavaScriptの理解しきれていない部分について改めて調べていたら思わず深みにはまりこんでしまった。
まだまだ、奥深いところがありそうだが、自分の理解の範囲ではこれ以上深い部分に触れる事は難しい。