このJavaScriptの解説は以下の本の2章と3章を元にしています。
Java開発者のためのAjax実践開発入門 河村嘉之、川尻剛、福沢知海(著) 技術評論社 ISBN978-4-7741-3297-6
javascriptにおいて、
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はプロトタイプベースのオブジェクト指向言語であり、 厳密にはクラスというものは存在しません。
関数オブジェクトに対して
普通の関数に対して 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を使って 参照できます。下の例では、新しく生成した空オブジェクトに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 に設定されたプロパティは、そのクラス(関数)から 派生した全てのインスタンスで共有されるようになります。
上記の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,'<') .replace(/>/g,'>') .replace(/"/g,'"') .replace(/'/g,''') }; var str = '<div>a</div>'; console.log(str.escapeHtml()); // <div>a</div> |
ただし、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の値を上書きすると、 他人が設定したprototypeの値を消してしまう可能性がある。
Admin.prototype.login = function() { ... } //他人が設定したプロパティ Admin.prototype = new User(); //上書きでlogin()メソッドが消えてしまう。 |
prototypeの値を上書きするのではなく、マージする必要がある。
var user = new User(); for (var p in user) { Admin.prototype[p] = user[p]; } |
javascriptではインスタンスの派生元を調べるのにconstructorというプロパティを使用する ことが多い。constructorはすべてのオブジェクトが備えるプロパティで、 コンストラクタ実行時に格納される。
console.log(user.constructor == User); // true |
console.log(admin.constructor == Admin); // false console.log(admin.constructor == User); // true |
このようにjavascriptで継承を直接扱うのはいろいろ難しい点も多いので、 prototype.js ライブラリを使うことが一般的のようだ。