不思議の国のJavaScript
オブジェクト指向に関するtips

《 初回公開:だいぶ昔で覚えていない , 最終更新:未 》


本稿は私もあまり理解しきれていない点について、自分の考えを整理するために書いているものなので他の記事もそうだが特に誤った事を書いている可能性が高いので注意していただきたい。

JavaScript には クラス という正式の概念がない。

JavaC# 等の通常のプログラミング言語における クラスベースのオブジェクト指向と呼ばれているが 、JavaScriptは プロトタイプベースの オブジェクト指向と呼ばれ変わった方法で実現されている。

クラスの定義

クラスFooを定義して、インスタンス化して変数fooに代入して操作する例を示します。

リスト1

01// ---------------------------------------- クラスの定義
02 
03// クラスのコンストラクタ
04function Foo(init_inst_member) {
05    // インンスtンス変数
06    this.inst_member = init_inst_member;
07}
08 
09// クラス変数
10Foo.cls_member = "クラス変数";
11// クラスメソッド
12Foo.cls_method = function() {
13    alert("クラスメソッドの実行")
14}
15 
16// インスタンスメソッド
17Foo.prototype.inst_method = function() {
18    alert("インスタンスメソッドの実行")
19}
20 
21// インスタンスメソッド
22Foo.prototype.inst_method2 = function() {
23    alert("二つ目インスタンスメソッドの実行")
24}
25 
26// 同名のインスタンス変数が宣言されていない場合にインスタンスで共通に利用される変数。
27Foo.prototype.inst_member_proto = "prototypeオブジェクトのプロパティ";
28 
29// ---------------------------------------- クラスを使う
30 
31var foo = new Foo("インスタンス変数の初期値")
32 
33alert(foo.inst_member)  // インスタンス変数の表示
34foo.inst_method();    // インスタンスメソッドの実行
35foo.inst_method2();      // 二つ目インスタンスメソッドの実行
36 
37alert(Foo.cls_member);  // クラス変数の表示
38Foo.cls_method();     // クラスメソッドの実行
39 
40// prototypeオブジェクトのプロパティをインスタンスプロパティとして使う。
41alert(foo.inst_member_proto); // 「prototypeオブジェクトのプロパティ」と表示
42 
43// このオブジェクトだけで使う事ができるメソッド(特異メソッド)
44foo.inst_method_ex = function() {
45    alert("特異メソッドの実行")
46}
47 
48foo.inst_method_ex(); // 特異メソッドの実行

クラスに付随するメンバーを定義するには、以下3種類のプロパティに変数や関数を代入する事で実現されている。
これらのどのプロパティに変数や関数を代入するかによってクラスのメンバーとして機能したりインスタンスのメンバーとなったりする。

  • コンストラクタである関数オブジェクト(この例ではFoo関数)のプロパティに変数や関数を代入する。
    クラス変数やクラスメソッドとなる。

  • コンストラクタである関数オブジェクトのprototypeプロパティのプロパティに変数や関数を代入する。
    クラスのインスタンスで共通に使える変数や関数(メソッド)となる。

  • インスタンス変数のプロパティに対して直接、変数や関数を代入する。
    そのインスタンスだけで使える固有の変数や関数(メソッド)となる。

    コンストラクタの中でthisのプロパティとして定義された変数や関数は基本的にはインスタンスメンバーとなるが、コンストラクタの外でインスタンス変数のプロパティとして定義された変数や関数は Ruby で言うところの特異メンバーに相当すると考えて良いと思う。

クラスのメンバーはすべてpublicとなりメンバーへのアクセス権の設定(privateやprotect)はできない。

prototypeオブジェクトのプロパティは特異なふるまいをするのだが、私ごときが下手な説明を加えてもかえって混乱を招くだけなので理解されているものとして次に進む。

applyメソッドとcallメソッド

上記のthisの例で関数の中のthisは通常はwindowオブジェクトを指している事が多いが、これを意識的に別のオブジェクトを指すように指定できる。

これにはFunctionオブジェクトのapplyメソッドかcallメソッドを使う。

applyメソッドとcallメソッドの違いはapplyメソッドが関数への引数を配列として渡す事です。

前項のサンプルプログラムの関数fooを以下のようにthisにHogiクラスのオブジェクトを指定して呼び出す。

リスト2

1var obj = new Hogi();
2foo.apply(obj);
3foo.call(obj);

applyメソッドとcallメソッドを引数を指定して呼び出す。

リスト3

01function foo(a,b,c) {
02    alert("this.prop="+this.prop+"\n"
03            +"a="+a+"\n"
04            +"b="+b+"\n"
05            +"c="+c+"\n");
06}
07 
08function Hogi() {
09    this.prop="Hogi.prop";
10}
11var obj = new Hogi();
12 
13foo.apply(obj,["arg_a","arg_b","arg_c"]);
14foo.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

1// 階乗n!を計算する
2var func=function(n){
3    if (n == 0)
4        return 1;
5    return arguments.callee(n - 1) * n;
6}
7 
8alert("10の階乗は"+func(10));

クラスの継承とミックスイン

前述のクラスFooを継承したChildFooクラスを実装したコードを以下に示す。

リスト5

01// ---------------------------------------- クラスの継承
02 
03// 子クラスのコンストラクタ
04function ChildFoo(init_inst_member) {
05    // コンストラクタチェーンを使って親クラスから継承したメンバーを初期化
06    Foo.call(this, init_inst_member);
07}
08 
09// プロトタイプチェーンで親クラスのメンバーを継承する。
10ChildFoo.prototype = new Foo();
11 
12// ChildFoo.prototypeはFooオブジェクトなのでFooクラスのメンバーが余分に
13// prototypeオブジェクトに含まれる事になるのでこれを駆除
14// 余分なだけで多分、削除しなくても問題は起こらない。
15delete ChildFoo.prototype.inst_member;
16 
17// プロトタイプチェーンによりconstructorプロパティがFooのコンストラクタに
18// 変更されているのでこれをもとの値に戻してやる。
19ChildFoo.prototype.constructor = ChildFoo;
20 
21// インスタンスメソッドをオーバライトする場合は、単にメソッドを再定義すれば良い。
22ChildFoo.prototype.inst_method2 = function() {
23    // 必要ならオーバライトした親クラスのメソーッドを
24    // 子クラスのメソッドとして実行する事もできる
25    Foo.prototype.inst_method2.call();
26 
27    alert("親クラスのインスタンスメソッドのオーバライト")
28}
29 
30// ---------------------------------------- 子クラスを使う
31 
32var childfoo = new ChildFoo("子クラスのインスタンス変数の初期値");
33 
34// 親クラスから継承したインスタンス変数を表示
35alert(childfoo.inst_member);
36 
37// 親クラスから継承したインスタンスメソッドを実行
38childfoo.inst_method();
39 
40// 親クラスから継承したインスタンスメソッドをオーバライトしたからメソッドを実行
41childfoo.inst_method2();
42 
43// 当然、親クラスのクラスメソッドを子クラスのクラスメソッドとして使う事はできない。
44//ChildFoo.cls_method(); // undefinedと表示される。

言語レベルで継承をサポートしていないので通常のプログラミング言語より複雑になってしまう。
これがプロトタイプベースのオブジェクト指向と呼ばれるゆえんである。

また、別のクラスのメンバーをコピーする事によって以下のようにクラスの継承の機能を擬似的に実現する事もできる。
これはRubyの多重継承を実現するための仕組みであるモジュールのインクルードに相当するものでRubyと同様 ミックスイン と呼ばれる。
これにより多重継承に似た機能が実現できる。
RubyのModuleクラスのincludeメソッドをまねてミックスインの実装をこころみたのが次の例です。

リスト6

01// ---------------------------------------- ミックスインによるクラスの継承
02 
03// ミックスイン機能を実現させるための関数
04// childClass : 子クラス
05// superClass : 継承元の親クラス
06// isConstructor 親クラスのコンストラクタを実行する場合はtrueを指定する。
07// args : コンストラクタの引数(isConstructorがtrueの場合に有効)
08function 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];
13        }
14    }
15 
16    // 親クラスのコンストラクタを実行してインスタンス変数を定義する。
17    if(isConstructor)
18        superClass.apply(childClass, args);
19}
20 
21// 子クラスのコンストラクタ
22function MixindFoo(init_inst_member) {
23    // インンスタンス変数
24    incledeClass(this, Foo, true, arguments);
25}
26 
27// ---------------------------------------- ミックスインを使う
28 
29var mixindFoo = new MixindFoo("ミックスインクラスのインスタンス変数の初期値")
30 
31// 親クラスから継承したインスタンス変数を表示
32alert(mixindFoo.inst_member);
33 
34// 親クラスから継承したインスタンスメソッドを表示
35mixindFoo.inst_method();

変数のいろいろ

JavaScriptではいろいろな種類の変数を使う事ができるので、混乱する事がある。
以下の例のように同じ変数名aの変数でもさまざま状況で使い分ける事はできる。

リスト7

01<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
02<html>
03<head>
04<meta http-equiv="Content-Type" content="text/html; charset=windows-31j">
05<title>変数宣言のいろいろ</title>
06</head>
07<body>
08<div id="message"></div>
09<script type="text/javascript">
10    // divタグに変数の値を表示させる関数
11    function addMessage(message) {
12        document.getElementById("message").innerHTML += message + "<br />";
13    }
14 
15    // グルーバル変数a (Windowオブジェクトのプロパティa)
16    var a = "グローバル変数";
17 
18    Foo.a = "クラス変数"
19    Foo.prototype.a = "Fooクラスのprototypeオブジェクトのプロパティa";
20    function Foo() {
21        var a = "ローカル変数";
22        this.a = "インスタンス変数";
23        addMessage("var a=" + a);
24        addMessage("this.a=" + this.a);
25        addMessage("window.a=" + window.a);
26    }
27 
28    function bar() {
29    }
30    bar.a = "関数barのプロパティa";
31 
32    var foo = new Foo();
33 
34    addMessage("a=" + a);
35    addMessage("Foo.a=" + Foo.a);
36    addMessage("Foo.prototype.a=" + Foo.prototype.a);
37    addMessage("foo.a=" + foo.a);
38 
39    addMessage("bar.a=" + bar.a);
40</script>
41</body>
42</html>

実行結果

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

01<body>
02<form name="form.name" onsubmit="return false;">
03<!-- ボタンのイベントの中のthisを確認 -->
04<button name="button.name" onclick="addMessage('button: ' + this.name);">
05    buttonイベントでのthis
06</button>
07</form>
08<div id="message"></div>
09<script type="text/javascript">
10    // divタグに変数の値を表示させる関数
11    function addMessage(message) {
12        document.getElementById("message").innerHTML += message + "<br />";
13    }
14 
15    var name = "グルーバル変数のname(window.name)";
16 
17    // トップレベルのthisが何を指しているか調べる
18    addMessage("1: " + this.name)
19    // windowオブジェクトがthisを指している事を確認する
20    addMessage("2: " + window.name)
21 
22    // 関数の中のthisが何を指している事を確認する
23    function foo() {
24        var name = "foo関数中のローカル変数"
25        addMessage("3: " + this.name)
26    }
27    foo();
28 
29    function Hogi() {
30        this.name = "hogi.name";
31        function bar() {
32            // コンストラクタの中のネスト関数のthisを確認
33            addMessage("4: " + this.name)
34        }
35        bar();
36        // コンストラクタの中のthisを確認
37        addMessage("5: " + this.name)
38    }
39    var hogi = new Hogi();
40</script>
41</body>

実行結果

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

1alert("typeof foo => "+typeof foo);   // objectが返される。

クラスのインスタンスかどうかを判断するためにinstanceof演算子を使う事ができます。

リスト10

1alert("foo instanceof Foo => "+ (foo instanceof Foo));      // trueを返す。

また、instanceof演算子はプロトタイプチェンを使ったクラスの継承関係にあるクラスに対してもtrueを返します。
しかし、プロトタイプチェンを使わないミックスインした親クラスに対してはfalseになってしまいます。

以下に例を示します。

リスト11

01// Objectクラスに対してもtrueを返す。
02alert("foo instanceof Object => "+ (foo instanceof Object));
03 
04// trueを返す。
05alert("childfoo instanceof ChildFoo => "+ (childfoo instanceof ChildFoo));
06// プロトタイプチェーンで繋がれた親クラスに対してもtrueを返す。
07alert("childfoo instanceof Foo => "+ (childfoo instanceof Foo));
08// Objectクラスに対しても当然trueを返す。
09alert("childfoo instanceof Object => "+ (childfoo instanceof Object));
10 
11// trueを返す。
12alert("mixindFoo instanceof MixindFoo => "+ (mixindFoo instanceof MixindFoo));
13// ミックスインによる継承ではプロトタイプチェーンではないのでfalseになる。
14alert("mixindFoo instanceof Foo => "+ (mixindFoo instanceof Foo));
15// すべてのオブジェクトはObjectクラスを継承するのでtrueとなる。
16alert("mixindFoo instanceof Object => "+ (mixindFoo instanceof Object));

クラスのprototype.constructorプロパティにはコンストラクタ関数が自動的に代入されています。
従って、継承関係を含まずに直接にどのクラスかを判断するにはconstructorプロパティが使えます。

リスト12

1// trueを返す。
2alert("foo.constructor==Foo => "+(foo.constructor==Foo));
3// foo.constructorはFooなので当然falseを返す。
4alert(" foo.constructor==Object => "+ (foo.constructor==Object));

但し、クラスの継承の例で示したようにconstructorプロパティは代入可能なため、作為的にconstructorプロパティに別の値を代入した場合にはこの関係は当然なりたたなくなります。

名前空間の利用

他のJavaScriptライブラリと名前の衝突が起こらないように 名前空間 を定義する必要があります。

名前空間はJavaScriptの言語仕様では定義されていません。
しかしJavaScriptのオブジェクトとプロパティの関係を利用すれば擬似的に名前空間を実現できます。

gudonとい名前空間のhogeいう関数を定義してこれを呼び出すには以下のようにします。

リスト13

01// ---------------------------------------- 名前空間
02 
03var gudon;
04if (!gudon)
05    gudon = {}
06else if (typeof gudon != "objec")
07    throw new Error("名前空間名gudonは既に使われています!!")
08 
09    // 名前空間gudonに関数hogeを定義
10gudon.hoge = function() {
11    // 関数の処理内容をここに記述する。
12    alert("call gudon.hoge");
13}
14 
15gudon.hoge(); // gudon名前空間のhogi関数を呼び出す。

このコードはgudonという名前のクローバル変数に空のオブジェクトを代入して新たにオブジェクトのプロパティhogeを定義して関数を代入しているのと同じです。

ここでは名前空間に関数を定義していますが、もちろんクラスや変数も定義できる。

名前空間をネストしてより名前の衝突の可能性を少なくする事ができます。
以下の例では名前空間gudonの下にさらにbarという名前空間を定義しています。

リスト14

01// ---------------------------------------- ネストした名前空間
02// gudon名前空間の下にさらにbarというネストした名前空間を定義
03 
04if (!gudon.bar)
05    gudon.bar = {}
06else if (typeof gudon.bar != "objec")
07    throw new Error("名前空間名gudon.barは既に使われています!!")
08 
09    // 名前空間gudon.barに関数hogeを定義
10gudon.bar.hoge = function() {
11    // 関数の処理内容をここに記述する。
12    alert("call gudon.bar.hoge");
13}
14 
15gudon.bar.hoge(); // gudon名前空間のhogi関数を呼び出す。

ネストした名前空間を使用すると名前が長くなってわずらわしい場合は別の変数に代入する事でこれを解決できる。

リスト15

1// 名前空間が長くなって使いづらい場合は別の変数に代入して使う事ができる。
2var func = gudon.baz.hoge;
3func();

最後に

JavaScriptの理解しきれていない部分について改めて調べていたら思わず深みにはまりこんでしまった。
まだまだ、奥深いところがありそうだが、自分の理解の範囲ではこれ以上深い部分に触れる事は難しい。

 

JavaScriptの参考書 /  Ajaxの参考書 /  HTMLの参考書 /  CSSの参考書

 

ページのトップへ戻る