JavaScript (2)

JavaScriptのオブジェクト指向


このJavaScriptの解説は以下の本の2章と3章を元にしています。

Java開発者のためのAjax実践開発入門
河村嘉之、川尻剛、福沢知海(著)
技術評論社
ISBN978-4-7741-3297-6

javascriptにおいて、this は「実行したメソッドを保持しているオブジェクト」 を指している。

    function test() { console.log(this.name); }
    var a = { name: 'a' };
    var b = { name: 'b' };
    a.method = test;
    b.method = test;
    console.log(a.method());   // a
    console.log(b.method());   // b

JavaScriptのオブジェクト指向

javascriptはプロトタイプベースのオブジェクト指向言語であり、 厳密にはクラスというものは存在しません。

関数に対してnew演算子を使用した例

関数オブジェクトに対して new演算子をつけて実行すると、 その関数はコンストラクタとして動作して、 新しい空のオブジェクトを生成して返します。 この新しいオブジェクトをインスタンスと呼びます。

普通の関数に対して new 演算子を適用できることが、次の例からわかります。

    function test(a,b) { return a * b; }
    var obj = new test(1,2);
    console.log(typeof(obj));  // object

オブジェクトを返す例

関数をコンストラクタとして実行した際に、return文でオブジェクトを返した場合は そのオブジェクトをインスタンスとして返します。 nullやundefinedはプリミティブ型なので、これらをreturnしても空のオブジェクトが 返ることに注意しましょう。

    function Test(a,b) { return { a: 1 }; }
    var obj = new Test();
    console.log(obj.a);  // 1

javascriptにおいてクラスは単なる関数オブジェクトなのですが、わかりやすくするため クラスの場合は大文字で始まる名前をつけておくと、プログラムが読みやすくなります。

コンストラクタでthisを使用した例

コンストラクタの内部では新しく生成した空オブジェクトを、thisを使って 参照できます。下の例では、新しく生成した空オブジェクトにaというプロパティを 設定しています。

    function Test() { this.a = 1; }
    var obj = new Test;  // javascriptでは引数が無い場合は関数の呼び出しの()を省略できる。
    console.log(obj.a);  // 1

インスタンスのプロパティに関数を代入した例

先の例では、新しく生成した空オブジェクトのプロパティの値として数値を代入しました。 もちろん、関数を代入することもできます。この関数をメソッドとみなして、 クラスのようなオブジェクトを定義することができます。

関数Userに"hanako"の文字列を引数としてnewすると、javascriptインタープリタは 空のオブジェクトを生成します。関数の中で別のオブジェクトをreturn しない限り、 このオブジェクトが返り値として使われることになります。 次に、コンストラクタの内容を実行して、オブジェクトの中にnameとsayNameという プロパティの値を設定します。 sayNameプロパティに設定された関数は、thisを使用して自分自身を保持する インスタンスのnameプロパティを参照します。 コンストラクタで指定しているthisと、sayNameメソッドの内部で指定している thisは結果的に同じものを参照しているが、意味が異なることに注意しましょう。

    function User(name) {
      this.name = name;
      this.sayName = function() { console.log(this.name); };
    }
    var user = new User("hanako");
    user.sayName();  // hanako

メモリ効率について(複数のインスタンスを生成する場合)

上記の例ではオブジェクトごとにsayName関数を生成することになるので, たとえば次のように複数個のオブジェクトを生成したときに それぞれ別のsayName()関数が生成されるのでメモリの使用に無駄が 生じてしまいます。

    var user1 = new User("hanako");
    var user2 = new User("umeko");
    console.log(user1.sayName === user2.sayName);  // false

同じ内容ならば同じものを参照するようにしたい場合は、prototype という 特殊なプロパティを使います。


プロトタイプチェーン

全ての関数は prototype という特殊なプロパティを持ち、通常はあるオブジェクトが 格納されています。このprototype に設定されたプロパティは、そのクラス(関数)から 派生した全てのインスタンスで共有されるようになります。

prototypeプロパティへの代入

上記のUserクラス(関数)において、sayNameが共有されるようにするには、次のように書きます。

    function User(name) { this.name = name; }
    User.prototype.sayName = function() { console.log(this.name); };
 
    var user1 = new User("hanako");
    var user2 = new User("umeko");
    user1.sayName();  // hanako
    console.log(user1.sayName === user2.sayName);  // true

クラスの拡張例

ObjectやStringといったjavascriptの組み込みクラスもprototype プロパティを 持つので、これらを使用して自分で機能を拡張することができます。

    String prototype.escapeHtml = function() {
      return this.replace(/&/g,'&')
                 .replace(/</g,'&lt;')
                 .replace(/>/g,'&gt;')
                 .replace(/"/g,'&quot;')
                 .replace(/'/g,'&#39;')
    };
    var str = '<div>a</div>';
    console.log(str.escapeHtml());  // &lt;div&gt;a&lt;/div&gt;

ただし、Objectクラスのprototypeを操作すると、 「どのオブジェクトでもにもそのプロパティが設定されてしまう」という 「オブジェクト汚染」を引き起こすので避けておいた方が無難です。

プロトタイプチェーン

全てのオブジェクトは [[Prototype]] という特殊なプロパティを持っています。 [[Prototype]]は仕様上の名前で、実際の名前は実装毎に異なり、 たとえばMozillaでは __proto__となっています。 [[Prototype]]は関数オブジェクトが持っている prototype とは別物であることに注意して下さい。

[[Prototype]]に格納された値もオブジェクトであるので、 やはり[[Prototype]]プロパティを持っています。 この[[Prototype]]のつながりをプロトタイプチェーンと呼びます。 [[Prototype]]の値がnullになったら終端です。

javascriptでは、プロパティの値を参照する時はプロトタイプチェーンを順にたどって 名前が一致するものを探します。 もしも見つからなかった場合(終端のnullに到達した場合)はundefinedを返します。 プロトタイプチェーンをたどるのはプロパティの参照時のみで、 値の更新時にはたどりません。 あるインスタンスでプロパティの値を設定すると、 そのインスタンスの中に新しいプロパティが作成されて値が設定されます。 つまり、プロパティチェーン中のプロパティは影響を受けませんし、 同じクラスに属する他のインスタンスも影響を受けません。

    function User(name) { this.name = name; }
    User.prototype.sayName = function() { console.log(this.name); };
 
    var user1 = new User("hanako");
    var user2 = new User("umeko");
    user1.sayName();  // hanako
    console.log(user1.sayName === user2.sayName);  // true
    user1.sayName = function() { console.log("hello"); };
    user1.sayName(); // hello
    user2.sayName();  // umeko

関数をコンストラクタとして実行すると、インタプリタは新しく生成した空オブジェクトの [[Prototype]]に、そのクラス(関数)のprototypeプロパティの値をコピーします。

継承の実現

関数オブジェクトのprototypeにプロパティを設定しておくと、 新しく生成したインスタンスの[[Prototype]]に値がコピーされるので、 新しいインスタンスでもそのプロパティにアクセスできることを説明してきました。

このprototypeにオブジェクトのインスタンスそのものを格納することで、 継承を実現できます。

    var Admin = function(name,pass) {
      User.apply(this,[name]);
      this.pass = pass;
    };
    Admin.prototype = new User();
    Admin.prototype.login = function() { console.log(this.pass); };

    var admin = new Admin("hanako", "pass");
    admin.sayName();  // hanako
    admin.login();    // pass

Adminクラスでは、applyメソッドを使用して親クラスであるUserを呼び出しています。 このメソッドはFunctionクラスで定義されたAPIで、引数に指定されたスコープと 引数で自分自身を実行します。 さらに、AdminクラスのprototypeをUserインスタンスで上書きして、 さらに独自のメソッドを格納しています。 これによりAdminインスタンスはUserクラスとAdminクラスの両方のメソッドを 扱うことができるようになります。

子クラスのprototypeプロパティに親クラスのインスタンスを代入することで起きる問題点

このようにjavascriptで継承を直接扱うのはいろいろ難しい点も多いので、 prototype.js ライブラリを使うことが一般的のようだ。