0x0b
js_about_07
最終更新:
0x0b
-
view
Working with Arrays
Creating an array
Working with array elements
Understanding length
Iterating over arrays
Array methods
Working with Array-like objects
Two-Dimensional Arrays
Array comprehensions
Iterators and Generators
Iterators
Defining custom iterators
Generators: a better way to build Iterators
Advanced generators
Generator expressions
オブジェクトモデルの詳細
クラスベース言語とプロトタイプベース言語
Java や C++ といったクラスベースのオブジェクト指向言語はクラスとインスタンスという 2 つの異なる実体があるという概念に基づいています。
クラスはあるオブジェクトの集合を特徴付けるすべてのプロパティ(Java ではメソッドとフィールドを、C++ ではメンバをプロパティと見なす)を定義する。クラスとはそれが表すオブジェクトの集合の特定のメンバではなく、抽象的なものである。例えば、Employee クラスで従業員すべてを含む集合を表す。
一方、インスタンスはクラスを実例にしたものである。つまり、そのメンバの 1 つということである。例えば、Victoria は Employee クラスのインスタンスとなることができる。このクラスは特定の個人を従業者として表すものである。インスタンスはその親クラスのプロパティを正確に保持する(それに他ならない)。
JavaScript のようなプロトタイプベース言語はこの区別がありません。単にオブジェクトがあるだけです。プロトタイプベース言語には原型的なオブジェクトという概念があります。このオブジェクトは新しいオブジェクトの初期プロパティを取得する元になるテンプレートとして使用されます。どのオブジェクトもそれ独自のプロパティを指定できます。オブジェクト作成時にも実行時にも可能です。さらに、どのオブジェクトも別のオブジェクトに対するプロトタイプとして関連付けることができます。2 つ目のオブジェクトが 1 つ目のオブジェクトのプロトタイプを共有するということもできます。
一方、インスタンスはクラスを実例にしたものである。つまり、そのメンバの 1 つということである。例えば、Victoria は Employee クラスのインスタンスとなることができる。このクラスは特定の個人を従業者として表すものである。インスタンスはその親クラスのプロパティを正確に保持する(それに他ならない)。
JavaScript のようなプロトタイプベース言語はこの区別がありません。単にオブジェクトがあるだけです。プロトタイプベース言語には原型的なオブジェクトという概念があります。このオブジェクトは新しいオブジェクトの初期プロパティを取得する元になるテンプレートとして使用されます。どのオブジェクトもそれ独自のプロパティを指定できます。オブジェクト作成時にも実行時にも可能です。さらに、どのオブジェクトも別のオブジェクトに対するプロトタイプとして関連付けることができます。2 つ目のオブジェクトが 1 つ目のオブジェクトのプロトタイプを共有するということもできます。
クラスの定義
クラスベース言語ではクラス定義ごとにクラスを定義します。定義では特殊なメソッドを指定してそのクラスのインスタンスを作成することができます。そのようなメソッドはコンストラクタと呼びます。コンストラクタメソッドはインスタンスのプロパティに対する初期値を指定することができます。また、作成時に他の適当な処理を実行することもできます。new 演算子をコンストラクタメソッドと一緒に用いることでクラスのインスタンスを作成できます。
JavaScript は同様のモデルに従っていますが、コンストラクタと別になっているクラス定義がありません。その代わりに、プロパティと値からなる特定の初期的なセットを持つオブジェクトを作成するコンストラクタ関数を定義します。どの JavaScript 関数もコンストラクタとして使用できます。new 演算子をコンストラクタ関数とともに使用することで新しいオブジェクトを作成します。
サブクラスと継承
クラスベース言語ではクラス定義を通じてクラスの階層を作ります。クラス定義では新しいクラスがある既存のクラスのサブクラスになるように指定することができます。サブクラスはスーパークラスの全プロパティを継承します。さらに新しくプロパティを追加したり継承したものを変更することもできます。例えば、Employee クラスが name および dept プロパティのみを含んでおり、Manager は reports プロパティを追加する Employee のサブクラスであるとします。この場合、Manager クラスのインスタンスは name、dept、reports という 3 つのプロパティをすべて持つことになります。
JavaScript では、原型的なオブジェクトをどのコンストラクタ関数にも結びつけることができるようにして継承を実装しています。そのため、全く同じような Employee と Manager の例を作成することができますが、使用する用語が若干異なります。まず、Employee コンストラクタ関数を定義します。これは name および dept プロパティを指定します。次に Manager コンストラクタ関数を定義します。これは reports プロパティを指定します。最後に新しい Employee オブジェクトを Manager コンストラクタ関数に対するプロトタイプとして代入します。そして新しい Manager を作成すると、このオブジェクトは Employee オブジェクトから name および dept プロパティを継承します。
プロパティの追加と削除
クラスベース言語では一般的にクラスをコンパイル時に生成し、コンパイル時または実行時にクラスのインスタンスを作成します。クラス定義後にそのクラスのプロパティの数や型を変更することはできません。しかし、JavaScript ではどんなオブジェクトでも実行時にプロパティを追加したり削除したりすることができます。あるオブジェクトのセットでプロトタイプとして使用されているオブジェクトにプロパティを追加すると、そのプロトタイプの使用元であるオブジェクトにも新しいプロパティが追加されます。
違いの概要
次の表でこれらの違いをいくつか短くまとめてみます。この章の残りで、JavaScript のコンストラクタとプロトタイプを用いてオブジェクト階層を作成することについての詳細を説明していきます。また、この方法が Java ではどう変わるかという比較もします。
クラスベース (Java) | プロトタイプベース (JavaScript) |
クラスとインスタンスは異なる実体である。 | すべてのオブジェクトはインスタンスである。 |
クラス定義を用いてクラスを定義する。また、コンストラクタメソッドを用いてクラスをインスタンス化する。 | コンストラクタ関数を用いてオブジェクトのセットを定義し、作成する。 |
new 演算子を用いて単一のオブジェクトを作成する。 | 同じ。 |
既存のクラスのサブクラスを定義するクラス定義を用いてオブジェクト階層を構築する。 | コンストラクタ関数に結びつけられたプロトタイプとしてオブジェクトを代入することでオブジェクト階層を構築する。 |
クラスチェーンに従ってプロパティを継承する。 | プロトタイプチェーンに従ってプロパティを継承する。 |
クラス定義がクラスの全インスタンスの全プロパティを指定する。実行時に動的にプロパティを追加することはできない。 | コンストラクタ関数またはプロトタイプがプロパティの初期セットを指定する。個々のオブジェクトやオブジェクトの全体のセットに動的にプロパティを追加したり、それらからプロパティを除去したりできる。 |
従業員の例
この章の残りは次の図で示す従業員の階層を使用していきます。
これの例では以下のオブジェクトを使用しています。
これの例では以下のオブジェクトを使用しています。
Employee はプロパティ name(デフォルトの値は空文字列)および dept(デフォルトの値は "general")を持つ。
Manager は Employee をベースとしている。これは reports プロパティ(デフォルトの値は空の配列、その値として Employee オブジェクトの配列を持たせる)を追加する。
WorkerBee も Employee をベースとしている。これは projects プロパティ(デフォルトの値は空の配列、その値として文字列の配列を持たせる)を追加する。
SalesPerson は WorkerBee をベースとしている。これは quota プロパティ(デフォルトの値は 100)を追加する。さらに dept プロパティを "sales" という値で上書きする。これは販売員は全員同じ部署に所属していることを示す。
Engineer は WorkerBee をベースとしている。これは machine プロパティ(デフォルトの値は空文字列)を追加し、さらに dept プロパティを "engineering" という値で上書きする。
残りの例:
Manager は Employee をベースとしている。これは reports プロパティ(デフォルトの値は空の配列、その値として Employee オブジェクトの配列を持たせる)を追加する。
WorkerBee も Employee をベースとしている。これは projects プロパティ(デフォルトの値は空の配列、その値として文字列の配列を持たせる)を追加する。
SalesPerson は WorkerBee をベースとしている。これは quota プロパティ(デフォルトの値は 100)を追加する。さらに dept プロパティを "sales" という値で上書きする。これは販売員は全員同じ部署に所属していることを示す。
Engineer は WorkerBee をベースとしている。これは machine プロパティ(デフォルトの値は空文字列)を追加し、さらに dept プロパティを "engineering" という値で上書きする。
残りの例:
- 階層の作成
- オブジェクトのプロパティ
- プロパティの継承
- プロパティの追加
- より柔軟なコンストラクタ
階層の作成
Employee の階層を実装するための適当なコンストラクタ関数を定義する方法はいくつかあります。これの定義に何を選択するかは、アプリケーションで何ができるようにしたいかに大きくよります。
このセクションではとても単純(かつ比較的柔軟でない)定義の使用方法を示し、継承を機能させる方法を実際に示します。これらの定義では、オブジェクト作成時に何らかのプロパティの値を指定することはできません。新しく作成されるオブジェクトは単にデフォルトの値を取得するだけです。これは後から変更できます。図 8.2 ではこれらの単純な定義を備えた階層を例示します。
実際のアプリケーションでは、オブジェクト作成時にプロパティの値を設定できるようにするコンストラクタを定義することになるでしょう(詳しくは より柔軟なコンストラクタ を参照)。今回はこれらの単純な定義を使用して、継承はどのようにして起こるのかを実際に示していくことにします。
Employee オブジェクトの定義
以下に示すように、Java と JavaScript の Employee の定義は似ています。唯一の相違点は、Java では各プロパティに対して型を指定する必要があるのに対して、JavaScript ではその必要がないことです。また、Java のクラスでは明示的なコンストラクタメソッドを作成する必要があります。
JavaScript Java
function Employee () {
this.name = "";
this.dept = "general";
}
public class Employee {
function Employee () {
this.name = "";
this.dept = "general";
}
public class Employee {
public String name; public String dept; public Employee () { this.name = ""; this.dept = "general"; }
}
Manager および WorkerBee の定義では、継承の連鎖において上である次のオブジェクトの指定方法に違いがあります。JavaScript では原型的なインスタンスをコンストラクタ関数の prototype プロパティとして追加します。コンストラクタを定義した後ならいつでもそれをすることができます。Java ではクラス定義内でスーパークラスを指定します。クラス定義の外部でスーパークラスを変更することはできません。
Manager および WorkerBee の定義では、継承の連鎖において上である次のオブジェクトの指定方法に違いがあります。JavaScript では原型的なインスタンスをコンストラクタ関数の prototype プロパティとして追加します。コンストラクタを定義した後ならいつでもそれをすることができます。Java ではクラス定義内でスーパークラスを指定します。クラス定義の外部でスーパークラスを変更することはできません。
JavaScript Java
function Manager () {
this.reports = [];
}
Manager.prototype = new Employee;
function Manager () {
this.reports = [];
}
Manager.prototype = new Employee;
function WorkerBee () {
this.projects = [];
}
WorkerBee.prototype = new Employee;
public class Manager extends Employee {
this.projects = [];
}
WorkerBee.prototype = new Employee;
public class Manager extends Employee {
public Employee[] reports; public Manager () { this.reports = new Employee[0]; }
}
public class WorkerBee extends Employee {
public String[] projects; public WorkerBee () { this.projects = new String[0]; }
}
Engineer および SalesPerson の定義は、WorkerBee の子孫、それゆえに Employee の子孫であるオブジェクトを作成します。これらの種類のオブジェクトは連鎖において上にある全オブジェクトのプロパティを持ちます。さらに、これらの定義は dept プロパティの継承された値をこれらのオブジェクト固有の新しい値で上書きします。
Engineer および SalesPerson の定義は、WorkerBee の子孫、それゆえに Employee の子孫であるオブジェクトを作成します。これらの種類のオブジェクトは連鎖において上にある全オブジェクトのプロパティを持ちます。さらに、これらの定義は dept プロパティの継承された値をこれらのオブジェクト固有の新しい値で上書きします。
JavaScript Java
function SalesPerson () {
function SalesPerson () {
this.dept = "sales"; this.quota = 100;
}
SalesPerson.prototype = new WorkerBee;
SalesPerson.prototype = new WorkerBee;
function Engineer () {
this.dept = "engineering"; this.machine = "";
}
Engineer.prototype = new WorkerBee;
public class SalesPerson extends WorkerBee {
Engineer.prototype = new WorkerBee;
public class SalesPerson extends WorkerBee {
public double quota; public SalesPerson () { this.dept = "sales"; this.quota = 100.0; }
}
public class Engineer extends WorkerBee {
public String machine; public Engineer () { this.dept = "engineering"; this.machine = ""; }
}
これらの定義を使用して、そのプロパティのデフォルト値を取得するこれらのオブジェクトのインスタンスを作成することができます。図 8.3 ではこれらの JavaScript の定義を使用して新しいオブジェクトを作成する方法を示しています。また、新しいオブジェクトに対するプロパティの値も示しています。
これらの定義を使用して、そのプロパティのデフォルト値を取得するこれらのオブジェクトのインスタンスを作成することができます。図 8.3 ではこれらの JavaScript の定義を使用して新しいオブジェクトを作成する方法を示しています。また、新しいオブジェクトに対するプロパティの値も示しています。
注意:インスタンスという用語はクラスベース言語においてはある特定の技術的な意味を持っています。これらの言語では、インスタンスとはクラスの個々のメンバであり、クラスとは根本的に異なるものです。JavaScript では「インスタンス」はこの技術的な意味を持っていません。なぜならば JavaScript にはクラスとインスタンスとの間のこの違いがないからです。しかしながら、JavaScript について話す際に、「インスタンス」をある特定のコンストラクタ関数を用いて作成したオブジェクトを意味する言葉として正式ではない形で使用することがあります。例えば、jane は Engineer のインスタンスであると砕けた言い方をすることもできます。同様に、親、子、祖先、そして子孫という用語は JavaScript において正式な意味を持ちませんが、プロトタイプチェーンにおいて上や下にあるオブジェクトについて言及する際にそれらを正式ではない形で使用してもかまいません。
図:単純な定義を用いたオブジェクトの作成
オブジェクトのプロパティ
このセクションでは、プロトタイプチェーンにおいてオブジェクトが他のオブジェクトからどのようにプロパティを継承するのか、また、実行時にプロパティを追加すると何が起きるのかについて論じます。
プロパティの継承
プロパティの追加
プロパティの追加
プロパティの継承
次の文を用いて(階層の作成 で示したように)mark オブジェクトを WorkerBee として作成するとします。
mark = new WorkerBee;
JavaScript は new 演算子に出くわすと、新しく汎用オブジェクトを生成し、この新しいオブジェクトを this キーワードの値として WorkerBee コンストラクタ関数に渡します。コンストラクタ関数は明示的に projects プロパティの値をセットします。さらに、内部的な __proto__ プロパティの値として WorkerBee.prototype の値をセットします。(このプロパティ名は最初と最後に 2 文字ずつのアンダースコアが付いています。)__proto__ プロパティはプロパティの値を返すのに使用されるプロトタイプチェーンを決定します。これらのプロパティがセットされると JavaScript は新しいオブジェクトを返し、代入文は変数 mark にそのオブジェクトをセットします。
このプロセスでは mark がプロトタイプチェーンから継承するプロパティとして明示的には mark オブジェクトに値(ローカルの値)を格納しません。プロパティの値を使用するとき、JavaScript はまずその値がそのオブジェクトに存在しているかどうかを確認します。存在している場合はその値が返されます。値がローカルには存在していない場合、JavaScript はプロトタイプチェーンを確認します(__proto__ プロパティを使用)。プロトタイプチェーン内のオブジェクトがそのプロパティの値を持っている場合、その値が返されます。そのようなプロパティが見つからない場合は JavaScript はそのオブジェクトにはそのプロパティがないと報告します。このようにして、mark オブジェクトには次のようなプロパティと値が入ることになります。
mark.name = ""; mark.dept = "general"; mark.projects = [];
mark オブジェクトは mark.__proto__ の原型的なオブジェクトから name および dept プロパティの値を継承します。WorkerBee コンストラクタによって projects プロパティにローカルの値が代入されます。このことでプロパティとその値を継承することができます。このプロセスの細かいところは プロパティの継承、再び にて議論します。
これらのコンストラクタにインスタンス固有の値を渡せないため、この情報は汎用的になります。プロパティの値は WorkerBee によって作成されるすべての新しいオブジェクトに共有される、デフォルトの値になります。もちろん、これらのどのプロパティのでもその値を変えることができます。そのためには次のようにして mark に固有の情報を与えます。
mark.name = "Doe, Mark"; mark.dept = "admin"; mark.projects = ["navigator"];
プロパティの追加
JavaScript では実行時にどんなオブジェクトにもプロパティを追加することができます。コンストラクタ関数で与えられるプロパティだけを使う必要はありません。ある 1 つのオブジェクト固有のプロパティを追加するには、次のようにしてオブジェクトに値を代入します。
mark.bonus = 3000;
すると、mark オブジェクトには bonus プロパティができます。しかし、他のどの WorkerBee にもこのプロパティは存在しません。
あるコンストラクタ関数に対するプロトタイプとして使用されているオブジェクトに新しいプロパティを追加する場合、プロトタイプからプロパティを継承する全オブジェクトへそのプロパティを追加することになります。例えば、次の文を使用すると specialty プロパティをすべての従業員に対して追加することができます。
Employee.prototype.specialty = "none";
JavaScript がこの文を実行するとすぐに mark オブジェクトも "none" という値を持つ specialty プロパティを持つようになります。次の図ではこのプロパティを Employee プロトタイプに追加し、さらに Engineer プロトタイプに対するそれを上書きしたときの効果を示します。
図 :プロパティの追加
より柔軟なコンストラクタ
これまでに見てきたコンストラクタ関数はインスタンス作成時にプロパティの値を指定することができませんでした。Java でのようにコンストラクタに引数を与えてインスタンスのプロパティの値を初期化することができます。次の図でそれを実現する 1 つの方法を示します。
図:コンストラクタでのプロパティの指定、その 1
次の表では Java および JavaScript でのこれらのオブジェクトの定義を示します。
JavaScript Java
function Employee (name, dept) {
function Employee (name, dept) {
this.name = name || ""; this.dept = dept || "general";
}
public class Employee {
public class Employee {
public String name; public String dept; public Employee () { this("", "general"); } public Employee (String name) { this(name, "general"); } public Employee (String name, String dept) { this.name = name; this.dept = dept; }
}
function WorkerBee (projs) {
this.projects = projs || [];
}
WorkerBee.prototype = new Employee;
public class WorkerBee extends Employee {
function WorkerBee (projs) {
this.projects = projs || [];
}
WorkerBee.prototype = new Employee;
public class WorkerBee extends Employee {
public String[] projects; public WorkerBee () { this(new String[0]); } public WorkerBee (String[] projs) { this.projects = projs; }
}
function Engineer (mach) {
this.dept = "engineering"; this.machine = mach || "";
}
Engineer.prototype = new WorkerBee;
public class Engineer extends WorkerBee {
Engineer.prototype = new WorkerBee;
public class Engineer extends WorkerBee {
public String machine; public WorkerBee () { this.dept = "engineering"; this.machine = ""; } public WorkerBee (String mach) { this.dept = "engineering"; this.machine = mach; }
}
これらの JavaScript の定義ではデフォルト値をセットするのに特殊なイディオムを使用しています。
this.name = name || "";
JavaScript の論理 OR 演算子 (||) はその最初の引数を評価します。その引数が true に変換できる場合、演算子はそれを返します。そうでない場合は第 2 の引数の値を返します。したがって、このコードは name が name プロパティに使用できる値であるかをテストします。そうであれば this.name にその値をセットします。そうでなければ this.name に空文字列をセットします。簡単のため、この章ではこのイディオムを使用します。しかしながら、一目見ただけでは不可解に思えるかもしれません。これは数値や真偽値の引数では期待どおりに動作しない可能性がありますので注意してください。これは 0(ゼロ)でも false でもデフォルト値が選択されるようになるからです。そのため、この場合では以下のより煩わしいイディオムを使用する必要が出てきます。このイディオムはすべてのデータ型で望みどおりに動作します。
this.authorized = typeof(authorized) !== 'undefined' ? authorized : true;
これらの定義を用いると、オブジェクトのインスタンスを作成するときに局所的に定義されたプロパティに対する値を指定することができます。図 8.5 で示したように、次の文を使用すると新しい Engineer を作成できます。
jane = new Engineer("belau");
すると Jane のプロパティは次のようになります。
jane.name == ""; jane.dept == "engineering"; jane.projects == []; jane.machine == "belau"
これらの定義を用いると name のような継承されたプロパティに対する初期値を指定することはできないので気をつけてください。JavaScript の継承されたプロパティに対する初期値を指定したいのであれば、コンストラクタ関数にさらにコードを追加する必要があります。
これまではコンストラクタ関数は汎用オブジェクトを生成し、その後に新しいオブジェクトに対するローカルプロパティと値を定義していました。プロトタイプチェーンにおいて上位のオブジェクトに対するコンストラクタ関数を直接呼び出すことで、コンストラクタにさらにプロパティを追加させることができます。次の図ではこれらの新しい定義を示します。
図 :コンストラクタでのプロパティの指定、その 2
これらの定義の 1 つを詳しく見ていきましょう。これは Engineer コンストラクタの新しい定義です。
function Engineer (name, projs, mach) { this.base = WorkerBee; this.base(name, "engineering", projs); this.machine = mach || ""; }
次のようにして新しい Engineer オブジェクトを作成するとします。
jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");
JavaScript は次のステップに従います。
1.new 演算子が汎用オブジェクトを生成し、その __proto__ プロパティに Engineer.prototype をセットする。
2.new 演算子が Engineer コンストラクタに新しいオブジェクトを this キーワードの値として渡す。
3.コンストラクタがそのオブジェクトに対する base という新しいプロパティを生成し、WorkerBee コンストラクタの値を base プロパティ代入する。このことで WorkerBee コンストラクタは Engineer オブジェクトのメソッドとなる。
2.new 演算子が Engineer コンストラクタに新しいオブジェクトを this キーワードの値として渡す。
3.コンストラクタがそのオブジェクトに対する base という新しいプロパティを生成し、WorkerBee コンストラクタの値を base プロパティ代入する。このことで WorkerBee コンストラクタは Engineer オブジェクトのメソッドとなる。
base というプロパティ名は特殊なものではありません。正当なプロパティ名であれば何でも使用できます。base は単にその意図をイメージさせるのに十分だからです。
4.コンストラクタが base メソッドを呼び出す。その引数として、コンストラクタに渡された引数のうちの 2 つ("Doe, Jane" および ["navigator", "javascript"])とさらに "engineering" という文字列を渡す。コンストラクタで "engineering" を明示的に使用することで、すべての Engineer オブジェクトが継承された dept プロパティに対して同じ値を持つようにし、この値が Employee から継承した値を上書きするようになる。
5.base は Engineer のメソッドであるため、base の呼び出し元の内部では JavaScript は this キーワードをステップ 1 で作成したオブジェクトに結びつける。したがって、WorkerBee 関数は順に "Doe, Jane" および ["navigator", "javascript"] という引数を Employee コンストラクタ関数に渡す。Employee コンストラクタ関数から戻ると、WorkerBee 関数は残りの引数を使用して projects プロパティをセットする。
6.base メソッドから戻ると、Engineer コンストラクタがオブジェクトの machine プロパティを "belau" で初期化する。
7.コンストラクタから戻ると、JavaScript は新しいオブジェクトを jane という変数に代入する。
Engineer コンストラクタの内部から WorkerBee コンストラクタを呼び出すと、Engineer オブジェクトに対して適切に継承をセットアップしたことになるのではないかと思うかもしれません。実際にはそうではありません。WorkerBee コンストラクタを呼び出すことで、呼び出された全コンストラクタ関数で指定されているプロパティを持った Engineer オブジェクトができるのが確実になります。しかし、後からプロパティを Employee または WorkerBee プロトタイプに追加する場合、そういったプロパティは Engineer オブジェクトに継承されません。例えば、次の文があるとします。
5.base は Engineer のメソッドであるため、base の呼び出し元の内部では JavaScript は this キーワードをステップ 1 で作成したオブジェクトに結びつける。したがって、WorkerBee 関数は順に "Doe, Jane" および ["navigator", "javascript"] という引数を Employee コンストラクタ関数に渡す。Employee コンストラクタ関数から戻ると、WorkerBee 関数は残りの引数を使用して projects プロパティをセットする。
6.base メソッドから戻ると、Engineer コンストラクタがオブジェクトの machine プロパティを "belau" で初期化する。
7.コンストラクタから戻ると、JavaScript は新しいオブジェクトを jane という変数に代入する。
Engineer コンストラクタの内部から WorkerBee コンストラクタを呼び出すと、Engineer オブジェクトに対して適切に継承をセットアップしたことになるのではないかと思うかもしれません。実際にはそうではありません。WorkerBee コンストラクタを呼び出すことで、呼び出された全コンストラクタ関数で指定されているプロパティを持った Engineer オブジェクトができるのが確実になります。しかし、後からプロパティを Employee または WorkerBee プロトタイプに追加する場合、そういったプロパティは Engineer オブジェクトに継承されません。例えば、次の文があるとします。
function Engineer (name, projs, mach) { this.base = WorkerBee; this.base(name, "engineering", projs); this.machine = mach || ""; } jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau"); Employee.prototype.specialty = "none";
jane オブジェクトは specialty プロパティを継承しません。動的な継承を確実にするにはやはりプロトタイプを明示的にセットアップする必要があります。代わりに次の文を使用するとします。
function Engineer (name, projs, mach) { this.base = WorkerBee; this.base(name, "engineering", projs); this.machine = mach || ""; } Engineer.prototype = new WorkerBee; jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau"); Employee.prototype.specialty = "none";
すると、jane オブジェクトの specialty プロパティの値は "none" になります。
もう 1 つの継承方法は .call/.apply メソッドを使うことです。以下のものは同等です。
function Engineer (name, projs, mach) { this.base = WorkerBee; this.base(name, "engineering", projs); this.machine = mach || ""; } function Engineer (name, projs, mach) { WorkerBee.call(this, name, "engineering", projs); this.machine = mach || ""; }
JavaScript の .call メソッドを使うことで実装がよりきれいになります。".base" が全く必要ないからです。
プロパティの継承、再び
これまでのセクションでは JavaScript のコンストラクタとプロトタイプが階層と継承をどのように実現しているかを説明してきました。このセクションでは、これまでの議論では必ずしも明白ではなかった細かい部分について議論していきます。
ローカル値と継承値
インスタンス関係の決定
コンストラクタにおけるグローバル情報
多重継承のようなもの
インスタンス関係の決定
コンストラクタにおけるグローバル情報
多重継承のようなもの
ローカル値と継承値
オブジェクトのプロパティにアクセスすると、この章で先に説明したように JavaScript は次のステップを実行します。
1.その値がローカルに存在するかを確かめる。存在している場合はその値を返す。
2.値がローカルには存在していない場合、プロトタイプチェーンを確認する(__proto__ プロパティを使用)。
3.プロトタイプチェーン内のオブジェクトが指定したプロパティの値を持っている場合、その値を返す。
4.そのようなプロパティが見つからなければ、そのオブジェクトにそのプロパティは存在しない。
このステップの結果はそれまでにどのようにオブジェクトを定義してきたかによります。元の例では次の定義を用いました。
2.値がローカルには存在していない場合、プロトタイプチェーンを確認する(__proto__ プロパティを使用)。
3.プロトタイプチェーン内のオブジェクトが指定したプロパティの値を持っている場合、その値を返す。
4.そのようなプロパティが見つからなければ、そのオブジェクトにそのプロパティは存在しない。
このステップの結果はそれまでにどのようにオブジェクトを定義してきたかによります。元の例では次の定義を用いました。
function Employee () { this.name = ""; this.dept = "general"; } function WorkerBee () { this.projects = []; } WorkerBee.prototype = new Employee;
この定義を前提とし、次の文を用いて WorkerBee のインスタンスとして amy を作成するとします。
amy = new WorkerBee;
amy オブジェクトにはローカルプロパティが 1 つあります。それは projects です。name および dept プロパティの値は amy にとってはローカルではないため、amy オブジェクトの __proto__ プロパティから取得されます。その結果、amy には次のプロパティ値が存在することになります。
amy.name == ""; amy.dept == "general"; amy.projects == [];
ここで、Employee に結びつけられたプロトタイプの name プロパティの値を変えるとします。
Employee.prototype.name = "Unknown"
一見、Employee の全インスタンスに新しい値が反映されるように思われます。しかしそうはなりません。
Employee オブジェクトのどんなインスタンスを作成しても、そのインスタンスは name プロパティに対するローカル値(空文字列)を持つことになります。新しい Employee オブジェクトを作成して WorkerBee プロトタイプをセットすると、WorkerBee.prototype は name プロパティに対するローカル値を持つということです。そのため、JavaScript が amy オブジェクト(WorkerBee のインスタンス)の name プロパティを探すと、JavaScript はそのプロパティに対するローカル値を WorkerBee.prototype 内で発見します。そして Employee.prototype へのチェーンの検索を停止します。
実行時にオブジェクトのプロパティの値を変更し、新しい値がそのオブジェクトのすべての子孫に継承されるようにしたい場合は、そのオブジェクトのコンストラクタ関数内でそのプロパティを定義してはいけません。その代わりにコンストラクタ関数に結びつけられたプロトタイプにそれを追加します。例えば、先のコードを次のように変更するとします。
function Employee () { this.dept = "general"; } Employee.prototype.name = ""; function WorkerBee () { this.projects = []; } WorkerBee.prototype = new Employee; amy = new WorkerBee; Employee.prototype.name = "Unknown";
この場合は amy の name プロパティは "Unknown" になります。
この例が示すように、オブジェクトのプロパティにデフォルト値を持たせ、実行時にデフォルト値を変更できるようにしたいのであれば、コンストラクタ関数自体の中ではなく、そのコンストラクタのプロトタイプでプロパティをセットするようにしてください。
インスタンス関係の決定
あるオブジェクトについて、プロトタイプチェーンにどんなオブジェクトがあるかを知りたい場面があるかもしれません。そうすることでこのオブジェクトはどのオブジェクトからプロパティを継承しているのかがわかります。
JavaScript 1.4 からは instanceof 演算子が導入され、プロトタイプチェーンをテストできるようになりました。この演算子は下で示す instanceOf 関数と全く同じように動作します。
プロパティの継承 で議論したように、コンストラクタ関数とともに new 演算子を用いて新しいオブジェクトを作成するとき、JavaScript は新しいオブジェクトの __proto__ プロパティにコンストラクタ関数の prototype プロパティの値をセットします。これを使用するとプロトタイプチェーンをテストすることができます。
例えば、既に示してある一連の定義を用います。プロトタイプもきちんとセットされているものとします。次のようにして __proto__ オブジェクトを作成します。
chris = new Engineer("Pigman, Chris", ["jsd"], "fiji");
このオブジェクトについて、以下の文はすべて true になります。
chris.__proto__ == Engineer.prototype; chris.__proto__.__proto__ == WorkerBee.prototype; chris.__proto__.__proto__.__proto__ == Employee.prototype; chris.__proto__.__proto__.__proto__.__proto__ == Object.prototype; chris.__proto__.__proto__.__proto__.__proto__.__proto__ == null;
ここで次のような instanceOf 関数を書いてみます。
function instanceOf(object, constructor) { while (object != null) { if (object == constructor.prototype) return true; object = object.__proto__; } return false; }
この定義を用いると、以下の式はすべて true になります。
instanceOf (chris, Engineer) instanceOf (chris, WorkerBee) instanceOf (chris, Employee) instanceOf (chris, Object)
しかし次の式は false になります。
instanceOf (chris, SalesPerson)
コンストラクタにおけるグローバル情報
コンストラクタを作成する際、コンストラクタ内でグローバル情報をセットする場合は注意が必要です。例えば、一意的な ID をそれぞれの新しい従業員情報に自動的に代入したいとします。そこで以下のように Employee を定義します。
var idCounter = 1; function Employee (name, dept) { this.name = name || ""; this.dept = dept || "general"; this.id = idCounter++; }
この定義を用いると、新しい Employee を作成するときに、コンストラクタに次の ID が順々に代入され、グローバル ID カウンタがインクリメントされます。その結果、以下の文を続けると victoria.id は 1 になり harry.id は 2 になります。
victoria = new Employee("Pigbert, Victoria", "pubs") harry = new Employee("Tschopik, Harry", "sales")
一見これは申し分なさそうです。しかし、idCounter はどんな用途であろうと Employee オブジェクトが作成されるたびにインクリメントされます。この章で示した Employee の階層全体を作成すると、Employee コンストラクタはプロトタイプをセットアップするたびに呼び出されます。次のコードを想定します。
var idCounter = 1; function Employee (name, dept) { this.name = name || ""; this.dept = dept || "general"; this.id = idCounter++; } function Manager (name, dept, reports) {...} Manager.prototype = new Employee; function WorkerBee (name, dept, projs) {...} WorkerBee.prototype = new Employee; function Engineer (name, projs, mach) {...} Engineer.prototype = new WorkerBee; function SalesPerson (name, projs, quota) {...} SalesPerson.prototype = new WorkerBee; mac = new Engineer("Wood, Mac");
さらに、ここでは省かれている定義に base プロパティがあり、その定義がプロトタイプチェーンにおいて上位のコンストラクタを呼び出すとします。この場合、mac オブジェクトが作成されるまでに mac.id が 5 になります。
カウンタが余計にインクリメントされることが問題になるかならないかは、そのアプリケーション次第です。このカウンタの正確な値が必要であれば、代わりに次のコンストラクタを使うという 1 つの解決策が考えられます。
function Employee (name, dept) { this.name = name || ""; this.dept = dept || "general"; if (name) this.id = idCounter++; }
プロトタイプとして使用するために Employee のインスタンスを作成するときはコンストラクタに引数を与えてはいけません。このコンストラクタの定義では、引数を渡さないときはコンストラクタが値を id に代入せず、カウンタを更新することもありません。そのため、Employee が id に値を代入するようにするときは従業員の名前を指定する必要があります。この例では mac.id は 1 になります。
多重継承のようなもの
オブジェクト指向言語の中には多重継承を許容するものもあります。つまり、オブジェクトは無関係な親オブジェクトからプロパティと値を継承できるということです。JavaScript は多重継承をサポートしていません。
実行時のプロパティ値の継承は、JavaScript が値を見つけようとオブジェクトのプロトタイプチェーンをサーチすることで行われます。オブジェクトに結びつけられたプロトタイプは 1 つであるため、JavaScript は複数のプロトタイプチェーンから動的に継承することはできません。
JavaScript ではコンストラクタ関数がその中で複数の別のコンストラクタ関数を呼び出すようにすることができます。これによって多重継承のようなものが実現できます。例えば以下の文があるとします。
function Hobbyist (hobby) { this.hobby = hobby || "scuba"; } function Engineer (name, projs, mach, hobby) { this.base1 = WorkerBee; this.base1(name, "engineering", projs); this.base2 = Hobbyist; this.base2(hobby); this.machine = mach || ""; } Engineer.prototype = new WorkerBee; dennis = new Engineer("Doe, Dennis", ["collabra"], "hugo")
さらに、WorkerBee の定義はこの章で先に使用したものであるとします。この場合 dennis オブジェクトにはこれらのプロパティが存在します。
dennis.name == "Doe, Dennis" dennis.dept == "engineering" dennis.projects == ["collabra"] dennis.machine == "hugo" dennis.hobby == "scuba" dennis は Hobbyist コンストラクタから hobby プロパティを取得しているのです。ここで Hobbyist コンストラクタのプロトタイプにプロパティを追加してみます。
Hobbyist.prototype.equipment = ["mask", "fins", "regulator", "bcd"]
こうしても dennis オブジェクトはこの新しいプロパティを継承しません。
こうしても dennis オブジェクトはこの新しいプロパティを継承しません。