★ wanayoo — archive 1999 https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Details_of_the_Object_ModelNouvelle recherche | Portail wanayoo
Your Search Results

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

    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 コンストラクタ関数を定義します。これは reports プロパティを指定します。最後に新しい Employee オブジェクトを、Manager コンストラクタ関数の prototype として割り当てます。そして新しい Manager を作成すると、このオブジェクトは Employee オブジェクトから name および dept プロパティを継承します。

    プロパティの追加と削除

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

    違いの概要

    次の表で、これらの違いのいくつかを短くまとめます。この章の残りで、JavaScript のコンストラクタとプロトタイプを用いてオブジェクト階層を作成することの詳細を説明します。また、この方法が Java ではどう変わるかという比較も行います。

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

    従業員の例

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

    図 8.1: 単純なオブジェクト階層

    この例では以下のオブジェクトを使用しています。

    • Employee はプロパティ name (デフォルトの値は空文字列) および dept (デフォルトの値は "general") を持ちます。
    • ManagerEmployee をベースとしています。これは reports プロパティ (デフォルトの値は空の配列、その値として Employee オブジェクトの配列を持たせます) を追加します。
    • WorkerBeeEmployee をベースとしています。これは projects プロパティ (デフォルトの値は空の配列、その値として文字列の配列を持たせます) を追加します。
    • SalesPersonWorkerBee をベースとしています。これは quota プロパティ (デフォルトの値は 100) を追加します。さらに dept プロパティを "sales" という値でオーバーライドします。これは、販売員が全員同じ部署に所属していることを示します。
    • EngineerWorkerBee をベースとしています。これは machine プロパティ (デフォルトの値は空文字列) を追加し、さらに dept プロパティを "engineering" という値でオーバーライドします。

    階層の作成

    Employee の階層を実装するための、適切なコンストラクタ関数を定義する方法はいくつかあります。これの定義に何を選択するかは、アプリケーションで何をできるようにした以下に大きく依存します。

    このセクションではとても単純 (かつ比較的柔軟でない) な定義の使用方法を示し、継承を機能させる方法を実際に示します。これらの定義では、オブジェクト作成時にプロパティの値を指定することはできません。新しく作成されるオブジェクトは単にデフォルトの値を取得するだけです。これは後から変更できます。図 8.2 では、これらの単純な定義を備えた階層を例示します。

    実際のアプリケーションでは、オブジェクト作成時にプロパティの値を指定できるコンストラクタを定義することになるでしょう (詳しくはより柔軟なコンストラクタをご覧ください)。今回はこれらの単純な定義を使用して、継承はどのようにして起こるのかを実際に示していくことにします。

    figure8.2.png
    図 8.2 : Employee オブジェクトの定義

    以下に示すように、Java と JavaScript の Employee の定義は似ています。両者の相違点は、Java では各プロパティに型を指定する必要があるのに対して、JavaScript ではその必要がないことです。また、Java のクラスでは明示的なコンストラクタメソッドを作成する必要があります。

    JavaScript Java
    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 では、クラス定義内でスーパークラスを指定します。クラス定義の外部でスーパークラスを変更することはできません。

    JavaScript Java
    function Manager () {
      this.reports = [];
    }
    Manager.prototype = new Employee;
    
    function WorkerBee () {
      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 プロパティの継承された値を、自オブジェクト固有の新しい値でオーバーライドします。

    JavaScript Java
    function SalesPerson () {
      this.dept = "sales";
      this.quota = 100;
    }
    SalesPerson.prototype = new WorkerBee;
    
    function Engineer () {
      this.dept = "engineering";
      this.machine = "";
    }
    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 の定義を使用して新しいオブジェクトを作成する方法を示しています。また、新しいオブジェクトのプロパティの値も示しています。

    註: インスタンスという用語は、クラスベースの言語においては特定の技術的な意味を持っています。これらの言語では、インスタンスとはクラスの個々のメンバであり、クラスとは根本的に異なるものです。JavaScript では、"インスタンス" にこのような技術的な意味はありません。なぜならば、JavaScript にはクラスとインスタンスとの間にそのような違いがないためです。しかしながら、JavaScript について話す際に "インスタンス" を、個々のコンストラクタ関数を用いて作成されたオブジェクトを意味する言葉として、正式ではない形で使用することがあります。例えば janeEngineer のインスタンスであると、砕けた言い方をすることもできます。同様に、祖先、そして子孫という用語は JavaScript において公式な意味を持ちませんが、プロトタイプチェーンにおいて上や下にあるオブジェクトについて言及する際に、それらを非公式に使用してもかまいません。

    figure8.3.png
    図 8.3 : 単純な定義を用いたオブジェクトの作成

    オブジェクトのプロパティ

    このセクションでは、プロトタイプチェーンにおいてオブジェクト型のオブジェクトからどのようにプロパティを継承するのか、また、実行時にプロパティを追加すると何が起きるのかについて論じます。

    プロパティの継承

    次の文を用いて (図 8.3 で示したように)、mark オブジェクトを WorkerBee として作成するとします:

    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 プロトタイプにあるプロパティをオーバーライドしたときの効果を示します。


    図 8.4 : プロパティの追加

    より柔軟なコンストラクタ

    これまでに見てきたコンストラクタ関数は、インスタンス作成時にプロパティの値を指定することができませんでした。Java のようにコンストラクタに引数を与えて、インスタンスのプロパティの値を初期化することができます。次の図で、それを実現する 1 つの方法を示します。


    図 8.5 : コンストラクタでのプロパティの指定、その 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 (ゼロ) や空文字列 ("") など) を伴って呼び出される場合に動作しないでしょう。この場合は、デフォルト値が選択されます。

    これらの定義を用いると、オブジェクトのインスタンスを作成するときに、局所的に定義されたプロパティに対する値を指定することができます。図 8.5 で示したように、次の文を使用すると新しい Engineer を作成できます:

    var jane = new Engineer("belau");
    

    すると Jane のプロパティは次のようになります:

    jane.name == "";
    jane.dept == "engineering";
    jane.projects == [];
    jane.machine == "belau"
    

    これらの定義では、name のような継承されたプロパティに対して初期値を指定することはできないので気をつけてください。JavaScript で継承されたプロパティに対して初期値を指定したいのであれば、コンストラクタ関数にさらにコードを追加する必要があります。

    これまではコンストラクタ関数は汎用オブジェクトを生成し、その後に新しいオブジェクトに対してローカルプロパティと値を定義していました。プロトタイプチェーンにおける上位のオブジェクトのコンストラクタ関数を直接呼び出すことで、コンストラクタへさらにプロパティを追加することができます。次の図ではこれらの新しい定義を示します。


    図 8.6 : コンストラクタでのプロパティの指定、その 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 演算子が Engineer コンストラクタに新しいオブジェクトを、this キーワードの値として渡します。
    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, 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. そのようなプロパティが見つからない場合は、オブジェクトにそのプロパティは存在しません。

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

    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 に値を代入せず、カウンタの更新も行いません。そのため、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;
    
    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 オブジェクトはこの新しいプロパティを継承しません。

    添付ファイル

    ファイル サイズ 日時 添付者:
    figure8.2.png
    9528 バイト 2010-05-31 16:22:19 user01
    figure8.3.png
    13984 バイト 2010-06-12 19:16:47 user01
    figure8.4.png
    18197 バイト 2010-06-10 16:13:52 user01
    figure8.5.png
    20879 バイト 2010-06-12 19:48:40 user01
    figure8.6.png
    24783 バイト 2010-06-14 21:53:54 user01
    figure8.1.png
    3395 バイト 2010-06-29 15:03:05 user01
    Variable panel (1)
    First shot of the variable panel
    89849 バイト 2012-11-29 09:50:21 Sheppy
    info
    info
    7478 バイト 2012-11-29 10:13:44 jbeatty

    Document Tags and Contributors

    Contributors to this page: akiroom, yyss, ethertank, Electrolysis
    最終更新者: ethertank,