オブジェクトモデルの詳細

この記事は技術レビューを必要としています。ぜひご協力ください

この記事は編集レビューを必要としています。ぜひご協力ください

JavaScript は、クラスではなく、プロトタイプに基づいたオブジェクトベースの言語です。この基本的な違いにより、JavaScript がオブジェクト階層構造をどのように作り上げているか、またプロパティやその値の継承方法が表面上分かりにくいものとなっています。本章ではこれらの実態を明らかにしていきます。

本章では、読者が JavaScript をある程度理解している、および単純なオブジェクトを作成するために JavaScript の関数を使用したことがあると想定しています。

クラスベース言語とプロトタイプベース言語

Java や C++ といったクラスベースのオブジェクト指向言語は、クラスとインスタンスという 2 種類の異なる実体があるという概念に基づいています。

  • クラスは、あるオブジェクトの集まりを特徴付けるすべてのプロパティ(Java ではメソッドとフィールドを、C++ ではメンバをプロパティとみなします)を定義します。クラスとは、自身を表すオブジェクト集合のメンバよりも、より抽象的なものです。例えば、Employee クラスは従業員すべての集合を表現することができます。
  • 一方、インスタンスはクラスを具体化したものです。つまり、クラスのメンバの 1 つです。例えば、VictoriaEmployee クラスのインスタンスになることができます。このインスタンスは、特定の個人を従業員として表すものです。インスタンスは、その親クラスのプロパティを(過不足なく)正確に保持します。

JavaScript のようなプロトタイプベースの言語は、この区別がありません。単にオブジェクトがあるだけです。プロトタイプベース言語には、プロトタイプオブジェクト (prototypical object) という概念があります。このオブジェクトは、新しいオブジェクトの初期プロパティの取得元になるテンプレートとして使用されます。どのオブジェクトも独自のプロパティを指定できます。これはオブジェクト作成時にも実行時にも可能です。さらに、どのオブジェクトも別のオブジェクトに対するプロトタイプとして関連づけることができます。2 つ目のオブジェクトに対し 1 つ目のオブジェクトのプロパティを共有させることもできます。

クラスの定義

クラスベース言語では、独立したクラス定義でクラスを定義します。定義ではコンストラクタと呼ばれる特殊なメソッドを使用して、そのクラスのインスタンスを作成することができます。コンストラクタメソッドは、インスタンスのプロパティに対する初期値を指定することができます。また、作成時に他の適切な処理を実行することもできます。new 演算子をコンストラクタメソッドと一緒に用いることで、クラスのインスタンスを作成できます。

JavaScript は同様のモデルに従っていますが、コンストラクタと別になっているクラス定義がありません。その代わりに、プロパティと値からなる特別な初期セットを持つオブジェクトを作成する、コンストラクタ関数を定義します。どの JavaScript 関数もコンストラクタとして使用できます。new 演算子をコンストラクタ関数とともに使用することで、新しいオブジェクトを作成します。

サブクラスと継承

クラスベース言語では、クラス定義を通してクラスの階層を作ります。クラス定義では、新しいクラスが既存のクラスのサブクラスになるよう指定することができます。サブクラスはスーパークラスの全プロパティを継承します。さらに、新たなプロパティの追加や継承したプロパティの変更もできます。例えば、Employee クラスが name および dept プロパティのみを含み、Managerreports プロパティが追加された Employee のサブクラスであるとします。この場合、Manager クラスのインスタンスは namedeptreports の 3 つのプロパティをすべて持つことになります。

JavaScript では、プロトタイプオブジェクトをどのコンストラクタ関数にも結びつけられるようにすることで、継承を実装しています。そのため、全く同じように EmployeeManager の例を作成できますが、使用する用語が若干異なります。まず、Employee コンストラクタ関数を定義します。この関数は name および dept プロパティを指定します。次に Manager コンストラクタ関数を定義します。この関数は Employee コンストラクタ関数を呼び出し、reports プロパティを指定します。最後に、Manager コンストラクト関数の prototypeEmployee.prototype から生成した新しいオブジェクトを代入します。そして新しい Manager を作成すると、このオブジェクトは Employee オブジェクトから name および dept プロパティを継承します。

プロパティの追加と削除

クラスベース言語では一般的にクラスをコンパイル時に生成し、コンパイル時または実行時にクラスのインスタンスを作成します。クラス定義後に、そのクラスのプロパティの数や型を変更することはできません。しかし JavaScript では、どのオブジェクトでも実行時にプロパティの追加や削除ができます。ある一連のオブジェクトでプロトタイプとして使用されているオブジェクトにプロパティを追加すると、それをプロトタイプとするオブジェクトにも新しいプロパティが追加されます。

相違点の概要

こうした相違点の要約を次表にまとめています。本章では後ほど、JavaScript のコンストラクタとプロトタイプを用いたオブジェクト階層作成の詳細を説明し、Java における手法との比較も行っていきます。

クラスベース (Java) とプロトタイプベース (JavaScript) のオブジェクトシステムの比較
クラスベース (Java) プロトタイプベース (JavaScript)
クラスとインスタンスは異なる実体です。 すべてのオブジェクトは別のオブジェクトを継承できます。
クラス定義を用いてクラスを定義します。また、コンストラクタメソッドを用いてクラスをインスタンス化します。 コンストラクタ関数を用いて一連のオブジェクトを定義および作成します。
new 演算子を用いて単一のオブジェクトを作成します。 同様です。
既存のクラスのサブクラスを定義するクラス定義を用いて、オブジェクト階層を構築します。 コンストラクタ関数に結びつけられたプロトタイプとしてオブジェクトを代入することで、オブジェクト階層を構築します。
クラスチェーンに従ってプロパティを継承します。 プロトタイプチェーンに従ってプロパティを継承します。
クラス定義が、クラスから作られた全インスタンスすべてのプロパティを定義します。実行時に動的にプロパティを追加することはできません。 コンストラクタ関数またはプロトタイプによって、一連の初期化されたプロパティが指定されます。個々のオブジェクトやオブジェクトのセット全体へ動的にプロパティを追加したり、それらからプロパティを削除したりできます。

事例 : 従業員モデル

ここからは、次の図で示す従業員の階層を使用していきます。

例で使用するオブジェクトの簡単な階層図 :

  • Employee には、プロパティ name(デフォルト値は空文字列)および dept(デフォルト値は "general")があります。
  • ManagerEmployee をベースとしています。reports プロパティ(デフォルト値は空の配列、値として Employee オブジェクトの配列を保持する)が追加されています。
  • WorkerBeeEmployee をベースとしています。projects プロパティ(デフォルト値は空の配列、値として文字列の配列を保持する)が追加されています。
  • SalesPersonWorkerBee をベースとしています。quota プロパティ(デフォルトの値は 100)が追加され、さらに dept プロパティを "sales" という値でオーバーライドします。これは、販売員が全員同じ部署に所属していることを示します。
  • EngineerWorkerBee をベースとしています。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 について話す際に「インスタンス」を、個々のコンストラクタ関数を用いて作成されたオブジェクトを意味する言葉として、正式ではない形で使用することがあります。例えば janeEngineer のインスタンスであると、砕けた言い方をすることもできます。同様に、「親」、「子」、「祖先」、そして「子孫」という用語は 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 引数の値を返します。したがって、このコードは namename プロパティの値に使用できる値かどうかを確認します。使用できると確認されれば 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 では次のような手順で事が運びます :

  1. new 演算子が汎用オブジェクトを生成し、その __proto__ プロパティに Engineer.prototype を設定します。
  2. new 演算子が this キーワードの値としてこの新しい汎用オブジェクトを Engineer コンストラクタに渡します。
  3. コンストラクタがそのオブジェクトに base という新しいプロパティを生成し、WorkerBee コンストラクタの値を base プロパティに代入します。これにより、WorkerBee コンストラクタは Engineer オブジェクトのメソッドになります。base というプロパティ名は特別なものではありません。あらゆる正当なプロパティ名を使用できますが、ここで base という名前を使うのは、その目的をたやすくイメージさせるためです。
  4. コンストラクタが base メソッドを呼び出します。その引数として、コンストラクタに渡された引数のうち 2 つ("Doe, Jane" および ["navigator", "javascript"])と、さらに文字列 "engineering" を渡します。コンストラクタで "engineering" を明示的に使用するのは、すべての Engineer オブジェクトは継承により dept プロパティは同じ値となっていて、Employee から継承された値を指定値にオーバーライドするためです。
  5. baseEngineer のメソッドであるため、base を呼び出す際に、JavaScript によって this キーワードをステップ 1 で作成したオブジェクトにバインドします。これにより、WorkerBee 関数は順に "Doe, Jane" および "engineering" という引数を 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 || "";
}
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 は次のステップを実行します :

  1. プロパティの値がローカルに存在するかを確かめます。存在している場合は、その値を返します。
  2. 値がローカルに存在していない場合は、プロトタイプチェーンを確認します(__proto__ プロパティを使用)。
  3. プロトタイプチェーン内のオブジェクトが指定したプロパティの値を持っている場合は、その値を返します。
  4. そのようなプロパティが見つからない場合は、オブジェクトにそのプロパティは存在しません。

このステップの結果は、それまでにどのようにオブジェクトを定義したかによります。元の例では次の定義を用いました :

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.prototypename プロパティのためのローカル値を持つことになる、ということです。そのため、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";

こうすれば、amyname プロパティは "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;
}
註: 上記の実装では、最近のバージョンでの JavaScript における XML オブジェクト表現法の癖を回避するために、オブジェクトの型と "xml" とを照合しています。具体的な詳細を知りたい場合は バグ 634150 をご覧ください。

この定義を用いると、以下の式はすべて 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"

dennisHobbyist コンストラクタから hobby プロパティを取得しているのです。ここで、Hobbyist コンストラクタのプロトタイプにプロパティを追加してみましょう :

Hobbyist.prototype.equipment = ["mask", "fins", "regulator", "bcd"]

このようにしても dennis オブジェクトはこの新しいプロパティを継承しません。

ドキュメントのタグと貢献者

 このページの貢献者: x2357, ethertank, akiroom, yyss, Electrolysis
 最終更新者: x2357,