この記事は技術レビューを必要としています。ぜひご協力ください。
この記事は編集レビューを必要としています。ぜひご協力ください。
JavaScript は、クラスではなく、プロトタイプに基づいたオブジェクトベースの言語です。この基本的な違いにより、JavaScript がオブジェクト階層構造をどのように作り上げているか、またプロパティやその値の継承方法が表面上分かりにくいものとなっています。本章ではこれらの実態を明らかにしていきます。
本章では、読者が JavaScript をある程度理解している、および単純なオブジェクトを作成するために JavaScript の関数を使用したことがあると想定しています。
クラスベース言語とプロトタイプベース言語
Java や C++ といったクラスベースのオブジェクト指向言語は、クラスとインスタンスという 2 種類の異なる実体があるという概念に基づいています。
- クラスは、あるオブジェクトの集まりを特徴付けるすべてのプロパティ(Java ではメソッドとフィールドを、C++ ではメンバをプロパティとみなします)を定義します。クラスとは、自身を表すオブジェクト集合のメンバよりも、より抽象的なものです。例えば、
Employee
クラスは従業員すべての集合を表現することができます。 - 一方、インスタンスはクラスを具体化したものです。つまり、クラスのメンバの 1 つです。例えば、
Victoria
はEmployee
クラスのインスタンスになることができます。このインスタンスは、特定の個人を従業員として表すものです。インスタンスは、その親クラスのプロパティを(過不足なく)正確に保持します。
JavaScript のようなプロトタイプベースの言語は、この区別がありません。単にオブジェクトがあるだけです。プロトタイプベース言語には、プロトタイプオブジェクト (prototypical object) という概念があります。このオブジェクトは、新しいオブジェクトの初期プロパティの取得元になるテンプレートとして使用されます。どのオブジェクトも独自のプロパティを指定できます。これはオブジェクト作成時にも実行時にも可能です。さらに、どのオブジェクトも別のオブジェクトに対するプロトタイプとして関連づけることができます。2 つ目のオブジェクトに対し 1 つ目のオブジェクトのプロパティを共有させることもできます。
クラスの定義
クラスベース言語では、独立したクラス定義でクラスを定義します。定義ではコンストラクタと呼ばれる特殊なメソッドを使用して、そのクラスのインスタンスを作成することができます。コンストラクタメソッドは、インスタンスのプロパティに対する初期値を指定することができます。また、作成時に他の適切な処理を実行することもできます。new
演算子をコンストラクタメソッドと一緒に用いることで、クラスのインスタンスを作成できます。
JavaScript は同様のモデルに従っていますが、コンストラクタと別になっているクラス定義がありません。その代わりに、プロパティと値からなる特別な初期セットを持つオブジェクトを作成する、コンストラクタ関数を定義します。どの JavaScript 関数もコンストラクタとして使用できます。new
演算子をコンストラクタ関数とともに使用することで、新しいオブジェクトを作成します。
サブクラスと継承
クラスベース言語では、クラス定義を通してクラスの階層を作ります。クラス定義では、新しいクラスが既存のクラスのサブクラスになるよう指定することができます。サブクラスはスーパークラスの全プロパティを継承します。さらに、新たなプロパティの追加や継承したプロパティの変更もできます。例えば、Employee
クラスが name
および dept
プロパティのみを含み、Manager
は reports
プロパティが追加された Employee
のサブクラスであるとします。この場合、Manager
クラスのインスタンスは name
、dept
、reports
の 3 つのプロパティをすべて持つことになります。
JavaScript では、プロトタイプオブジェクトをどのコンストラクタ関数にも結びつけられるようにすることで、継承を実装しています。そのため、全く同じように Employee
と Manager
の例を作成できますが、使用する用語が若干異なります。まず、Employee
コンストラクタ関数を定義します。この関数は name
および dept
プロパティを指定します。次に Manager
コンストラクタ関数を定義します。この関数は Employee
コンストラクタ関数を呼び出し、reports
プロパティを指定します。最後に、Manager
コンストラクト関数の prototype
に Employee.prototype
から生成した新しいオブジェクトを代入します。そして新しい 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" という値でオーバーライドします。
階層の作成
Employee の階層を実装するための、適切なコンストラクタ関数を定義する方法はいくつかあります。定義の方法に何を選択するかは、アプリケーションで何を可能にしたいかに大きく依存します。
この章では、継承がどのように機能するかを表現するため、とても単純な(かつ比較的柔軟でない)定義の使い方でこれを説明していきます。この定義では、オブジェクト作成時にプロパティの値を指定することはできません。新しく作成されるオブジェクトは単にデフォルトの値を取得するだけで、値は後から変更できます。
実際のアプリケーションでは、オブジェクト作成時にプロパティの値を指定できるコンストラクタを定義することになるでしょう (詳しくはより柔軟なコンストラクタをご覧ください)。今回はこれらの単純な定義を使用して、継承はどのようにして起こるのかを実際に示していくことにします。
以下に示すように、Java と JavaScript の Employee
の定義は似ています。両者の相違点は、Java では各プロパティに型を指定する必要があるのに対して、JavaScript ではその必要がないことです(これは JavaScript が弱い型付けの言語であるのに対して Java が 強い型付け言語だからです)。
JavaScript
function Employee() { this.name = ""; this.dept = "general"; }
Java
public class Employee { public String name = ""; public String dept = "general"; }
Manager
および WorkerBee
の定義では、継承チェーンにおいて隣接する上位オブジェクトの指定方法に違いがあります。JavaScript ではプロトタイプインスタンスを、コンストラクタ関数の prototype
プロパティの値として追加します。コンストラクタを定義した後なら、いつでもこれを行うことができます。Java では、クラス定義内でスーパークラスを指定します。クラス定義の外部でスーパークラスを変更することはできません。
JavaScript
function Manager() { Employee.call(this); this.reports = []; } Manager.prototype = Object.create(Employee.prototype); function WorkerBee() { Employee.call(this); this.projects = []; } WorkerBee.prototype = Object.create(Employee.prototype);
Java
public class Manager extends Employee { public Employee[] reports = new Employee[0]; } public class WorkerBee extends Employee { public String[] projects = new String[0]; }
Engineer
および SalesPerson
の定義は、WorkerBee
の子孫、したがって Employee
の子孫でもあるオブジェクトを作成します。こうした種類のオブジェクトは、チェーンの上位にある全オブジェクトのプロパティを持ちます。さらに、これらの定義によって、継承された dept
のプロパティ値を、自身のオブジェクト固有の新しい値にオーバーライドしています。
JavaScript
function SalesPerson() { WorkerBee.call(this); this.dept = "sales"; this.quota = 100; } SalesPerson.prototype = Object.create(WorkerBee.prototype); function Engineer() { WorkerBee.call(this); this.dept = "engineering"; this.machine = ""; } Engineer.prototype = Object.create(WorkerBee.prototype);
Java
public class SalesPerson extends WorkerBee { public double quota; public dept = "sales"; public quota = 100.0; } public class Engineer extends WorkerBee { public String machine; public dept = "engineering"; public machine = ""; }
これらの定義を使用して、プロパティがデフォルト値をとる、オブジェクトのインスタンスを作成することができます。下記の図は、これらの JavaScript の定義を使用して新しいオブジェクトを作成する方法を示しています。また、新しいオブジェクトのプロパティの値も示しています。
註: インスタンスという用語は、クラスベースの言語においては特定の技術的な意味を持っています。これらの言語では、インスタンスとはクラスの個々のメンバであり、クラスとは根本的に異なるものです。JavaScript では、「インスタンス」にこのような技術的な意味はありません。なぜならば、JavaScript にはクラスとインスタンスとの間にそのような違いがないためです。しかしながら、JavaScript について話す際に「インスタンス」を、個々のコンストラクタ関数を用いて作成されたオブジェクトを意味する言葉として、正式ではない形で使用することがあります。例えば jane
は Engineer
のインスタンスであると、砕けた言い方をすることもできます。同様に、「親」、「子」、「祖先」、そして「子孫」という用語は JavaScript において公式な意味を持ちませんが、プロトタイプチェーンにおいて上や下にあるオブジェクトについて言及する際に、それらを非公式に使用してもかまいません。
簡単な定義によるオブジェクトの作成
オブジェクト階層
下記のような階層が、右に書かれたコードを使って作成されます。
個別のオブジェクト
var jim = new Employee; // jim.name is '' // jim.dept is 'general' var sally = new Manager; // sally.name is '' // sally.dept is 'general' // sally.reports is [] var mark = new WorkerBee; // mark.name is '' // mark.dept is 'general' // mark.projects is [] var fred = new SalesPerson; // fred.name is '' // fred.dept is 'sales' // fred.projects is [] // fred.quota is 100 var jane = new Engineer; // jane.name is '' // jane.dept is 'engineering' // jane.projects is [] // jane.machine is ''
オブジェクトのプロパティ
この章では、オブジェクトがどのようにしてプロトタイプチェーンにより他のオブジェクトからプロパティを継承するのか、また実行時にプロパティを追加すると何が起きるのかについて考察します。
プロパティの継承
次の文を用い、 WorkerBee
として mark
オブジェクトを作成するとしましょう :
var 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
プロパティの値を継承します。projects
プロパティは、WorkerBee
コンストラクタによってローカルの値が代入されます。JavaScript ではこのようにプロパティとその値の継承を行います。このプロセスの詳細はプロパティの継承、再びにて説明します。
これらのコンストラクタはインスタンス固有の値を渡せないため、この情報は汎用的になります。プロパティの値は、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) { this.name = name || ""; this.dept = dept || "general"; }
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 { public String[] projects; public WorkerBee () { this(new String[0]); } public WorkerBee (String[] projs) { projects = projs; } }
function Engineer (mach) { this.dept = "engineering"; this.machine = mach || ""; } Engineer.prototype = new WorkerBee;
public class Engineer extends WorkerBee { public String machine; public Engineer () { dept = "engineering"; machine = ""; } public Engineer (String mach) { dept = "engineering"; machine = mach; } }
これらの JavaScript の定義では、デフォルト値の設定に特殊なイディオムを使用しています :
this.name = name || "";
JavaScript の論理和 (OR) 演算子 (||
) は、その最初の引数を評価します。その引数が true に評価される場合、演算子はその引数を返します。そうでない場合、第 2 引数の値を返します。したがって、このコードは name
が name
プロパティの値に使用できる値かどうかを確認します。使用できると確認されれば this.name
にその値を設定します。そうでなければ this.name
に空文字列をセットします。本章ではこの方法がより簡潔なのでこのイディオムを使用していますが、一目見ただけでは不可解に思えるかもしれません。
false
に変換される引数(0
や空文字列 (""
) など)といっしょにコンストラクタ関数が呼び出された場合、動作しません。その場合はデフォルト値が選択されます。これらの定義を用いると、オブジェクトのインスタンスを作成するときに、局所的に定義されたプロパティに対する値を指定することができます。次の文を使用すると新しい Engineer
を作成できます :
var 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
オブジェクトを作成するとします :
var jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");
JavaScript では次のような手順で事が運びます :
new
演算子が汎用オブジェクトを生成し、その__proto__
プロパティにEngineer.prototype
を設定します。new
演算子がthis
キーワードの値としてこの新しい汎用オブジェクトをEngineer
コンストラクタに渡します。- コンストラクタがそのオブジェクトに
base
という新しいプロパティを生成し、WorkerBee
コンストラクタの値をbase
プロパティに代入します。これにより、WorkerBee
コンストラクタはEngineer
オブジェクトのメソッドになります。base
というプロパティ名は特別なものではありません。あらゆる正当なプロパティ名を使用できますが、ここでbase
という名前を使うのは、その目的をたやすくイメージさせるためです。 - コンストラクタが
base
メソッドを呼び出します。その引数として、コンストラクタに渡された引数のうち 2 つ("Doe, Jane"
および["navigator", "javascript"]
)と、さらに文字列"engineering"
を渡します。コンストラクタで"engineering"
を明示的に使用するのは、すべてのEngineer
オブジェクトは継承によりdept
プロパティは同じ値となっていて、Employee
から継承された値を指定値にオーバーライドするためです。 base
はEngineer
のメソッドであるため、base
を呼び出す際に、JavaScript によってthis
キーワードをステップ 1 で作成したオブジェクトにバインドします。これにより、WorkerBee
関数は順に"Doe, Jane"
および"engineering"
という引数をEmployee
コンストラクタ関数に渡します。Employee
コンストラクタ関数から戻ると、WorkerBee
関数は残りの引数を使用してprojects
プロパティをセットします。base
メソッドから戻ると、Engineer
コンストラクタがオブジェクトのmachine
プロパティを"belau"
に初期化します。- コンストラクタから戻ると、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 || ""; } var 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; var jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau"); Employee.prototype.specialty = "none";
すると、jane
オブジェクトの specialty
プロパティの値は "none" になります。
もう 1 つの継承方法は、call()
/ apply()
メソッドを使う方法です。以下のコードの内容は同じものとなります :
function Engineer (name, callprojs, 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 は次のステップを実行します :
- プロパティの値がローカルに存在するかを確かめます。存在している場合は、その値を返します。
- 値がローカルに存在していない場合は、プロトタイプチェーンを確認します(
__proto__
プロパティを使用)。 - プロトタイプチェーン内のオブジェクトが指定したプロパティの値を持っている場合は、その値を返します。
- そのようなプロパティが見つからない場合は、オブジェクトにそのプロパティは存在しません。
このステップの結果は、それまでにどのようにオブジェクトを定義したかによります。元の例では次の定義を用いました :
function Employee () { this.name = ""; this.dept = "general"; } function WorkerBee () { this.projects = []; } WorkerBee.prototype = new Employee;
この定義を前提とし、次の文を用いて WorkerBee
のインスタンスとして amy
を作成するとします :
var 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; var amy = new WorkerBee; Employee.prototype.name = "Unknown";
こうすれば、amy
の name
プロパティは "Unknown" になります。
この例で示したように、オブジェクトのプロパティにデフォルトの値を持たせて、実行時にデフォルト値を変更したいのであれば、コンストラクタ関数内でなく、コンストラクタのプロトタイプ内でプロパティを設定するようにしてください。
インスタンス関係の決定
JavaScript でのプロパティ探索は、まずオブジェクト自身のプロパティ内で探索し、そのプロパティ名が存在しない場合は特殊なオブジェクトプロパティである __proto__
で探索します。これは再帰的に継続されます。このプロセスを「プロトタイプチェーンの探索」と呼びます。
この特別なプロパティ __proto__
は、オブジェクトが構築される際に設定されて、コンストラクタの prototype
プロパティを構成する値となります。よって、式 new Foo()
は __proto__ ==
となるオブジェクトを作成します。その結果、Foo.prototype
Foo.prototype
のプロパティの変更により、new Foo()
で作成されたすべてのオブジェクトのプロパティ探索が変更されます。
すべてのオブジェクトは __proto__
オブジェクトプロパティを持ちます(Object
を除いて)。また、すべての関数は prototype
オブジェクトプロパティを持ちます。したがって、「プロトタイプ継承」を用いてオブジェクトを別のオブジェクトへ関連づけられます。オブジェクトの __proto__
と関数の prototype
オブジェクトを比較することで、継承状態の確認ができます。これを行う手っ取り早い方法が JavaScript にはあります。instanceof
演算子はオブジェクトと関数を検査して、オブジェクトが関数のプロトタイプから継承している場合に true を返します。例えば、
var f = new Foo(); var isTrue = (f instanceof Foo);
詳例として、プロパティの継承 で利用した定義を使ってみましょう。以下のようにして Engineer
オブジェクトを作成しましょう :
var 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; } if (typeof object == 'xml') { return constructor.prototype == XML.prototype; } 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 となります :
var victoria = new Employee("Pigbert, Victoria", "pubs") var 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; var 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 に値を代入せず、カウンタの更新も行いません。そのため、割り当てられる id を Employee
に付与したい場合は、従業員の名前を指定する必要があります。この例では 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; var 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
オブジェクトはこの新しいプロパティを継承しません。