プログラミングでは、何かを取ってそれを拡張することがしばしばあります。

例えば、プロパティとメソッドをもつ user オブジェクトを持っているとします。そして、そのいくつかを僅かに変更した adminguest を作りたいとします。コピーや再実装ではなく、単にその上に新しいオブジェクトを作成することで、user が持っているものを再利用したいです。

プロトタイプ継承 はそれを助ける言語の機能です。

プロトタイプ [[Prototype]]

JavaScriptでは、オブジェクトは特別な隠しプロパティ [[Prototype]] を持っており、それは null または別のオブジェクトを参照します。そのオブジェクトは “プロトタイプ” と呼ばれます。

[[Prototype]] は “魔法のような” 意味を持っています。私たちが object からプロパティを読みたいときで、それがない場合、JavaScriptは自動的に、プロトタイプからそれを取得します。プログラミングではこのようなことを “プロトタイプ継承” と呼びます。多くのクールな言語機能やプログラミングテクニックは、これがベースになっています。

プロパティ [[Prototype]] は内部であり隠されていますが、セットする多くの方法があります。

それらの1つは、次のように __proto__ を使う方法です:

let animal = {
  eats: true
};
let rabbit = {
  jumps: true
};

rabbit.__proto__ = animal;

__proto__[[Prototype]]同一ではない ことに注意してください。これはそのための getter/setter です。あとでセットする別の方法について話しますが、今のところ __proto__ の理解はそれで問題ありません。

もしも rabbit の中のプロパティを探し、それがない場合、JavaScriptは自動で animal からそれを取ります。

例:

let animal = {
  eats: true
};
let rabbit = {
  jumps: true
};

rabbit.__proto__ = animal; // (*)

// 今、rabbit で両方のプロパティを見つけることができます:
alert( rabbit.eats ); // true (**)
alert( rabbit.jumps ); // true

ここで、行 (*)rabbit のプロトタイプに animal をセットしています。

次に、alert がプロパティ rabbit.eats (**) を読もうとしたとき、それは rabbit にはないので、JavaScriptは [[Prototype]] 参照に従って、animal の中でそれを見つけます(下から上に向かいます)。

ここでは、私たちは "animalrabbit のプロトタイプ" または "rabbit がプロトタイプ的に animal を継承している" という事ができます。"

したがって、もし animal が多くの役立つプロパティやメソッドを持っている場合、それらは自動的に rabbit でも利用可能になります。 このようなプロパティは “継承” と呼ばれます。

もし animal がメソッドを持っている場合、rabbit でもそれを呼ぶことができます:

let animal = {
  eats: true,
  walk() {
    alert("Animal walk");
  }
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

// walk は prototype から取られました
rabbit.walk(); // Animal walk

メソッドは次のように自動的にプロトタイプから取られます。:

プロトタイプチェーンは長くても問題ありません。:

let animal = {
  eats: true,
  walk() {
    alert("Animal walk");
  }
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

let longEar = {
  earLength: 10,
  __proto__: rabbit
}

// walk は prototype チェーンから取られました
longEar.walk(); // Animal walk
alert(longEar.jumps); // true (rabbit から)

実際には、2つの制限があります。:

  1. 参照を循環させることはできません。JavaScriptは、循環するように __proto__ を割り当てようとするとエラーを投げます。
  2. __proto__ の値はオブジェクトまたは null になります。プリミティブのような、それ以外のすべての値は無視されます。

また、それは明らかかもしれませんが、1つの [[Prototype]] しか存在しません。 オブジェクトは2つの他のものから継承することはできません。

読み書きのルール

プロトタイプは、プロパティを読むためだけに使われます。

データプロパティ(getter/setter ではない)の場合、書き込み/削除操作はオブジェクトで直接動作します。

下の例では、自身の walk メソッドを rabbit に割り当てています。:

let animal = {
  eats: true,
  walk() {
    /* このメソッドは rabbit では使われません */
  }
};

let rabbit = {
  __proto__: animal
}

rabbit.walk = function() {
  alert("Rabbit! Bounce-bounce!");
};

rabbit.walk(); // Rabbit! Bounce-bounce!

これ以降、rabbit.walk() 呼び出しは、プロトタイプを使うことなく、オブジェクトの中にすぐにメソッドを見つけ、それを実行します。

getter/setter の場合 – もしプロパティの読み書きをすると、プロトタイプで参照されて呼び出されます。

例えば、以下のコードで admin.fullName プロパティをチェックしてください:

let user = {
  name: "John",
  surname: "Smith",

  set fullName(value) {
    [this.name, this.surname] = value.split(" ");
  },

  get fullName() {
    return `${this.name} ${this.surname}`;
  }
};

let admin = {
  __proto__: user,
  isAdmin: true
};

alert(admin.fullName); // John Smith (*)

// setter がトリガします!
admin.fullName = "Alice Cooper"; // (**)

ここで、行 (*) では、プロパティ admin.fullName はプロトタイプ user が getter を持っているので、それが呼ばれます。また、行 (**) では、プロパティはプロトタイプに setter を持っているので、それが呼ばれます。

“this” の値

上の例で、興味深い質問が起きるかもしれません。: set fullName(value) の内側での this の値はなんでしょうか? プロパティ this.namethis.surname が書かれているのはどこでしょうか? user または admin ?

答えはシンプルです: this はプロトタイプによる影響を持ったく受けません。

メソッドがどこにあるかは関係ありません:オブジェクトの中でも、そのプロトタイプ内でも。メソッド呼び出しでは、this は常にドットの前のオブジェクトです。

したがって、setter は実際に this として admin を使い、user ではありません。

それは、実際には非常に重要なことです。なぜなら、多くのメソッドを持つ大きなオブジェクトを持ち、それを継承する可能性があるからです。次に、継承されたオブジェクトの上でそれらのメソッドを実行し、大きなオブジェクトではなく、継承したオブジェクトの状態を変更します。

例えば、ここでは animal は “メソッド格納域” を表現しており、rabbit はそれを使います。

呼び出し rabbit.sleep()rabbit オブジェクトに this.isSleeping をセットします。:

// animal がメソッドを持っています
let animal = {
  walk() {
    if (!this.isSleeping) {
      alert(`I walk`);
    }
  },
  sleep() {
    this.isSleeping = true;
  }
};

let rabbit = {
  name: "White Rabbit",
  __proto__: animal
};

// rabbit.isSleeping を変更する
rabbit.sleep();

alert(rabbit.isSleeping); // true
alert(animal.isSleeping); // undefined (prototype にそのようなプロパティはありません)

結果の図は次のようになります:

もしも私たちが bird, snake など animal から継承された他のオブジェクトを持っていた場合、それらもまた animal のメソッドへのアクセスを得ます。しかし各メソッドでの this は対応するオブジェクトであり、animal ではなく、呼び出し時に(前のドット)で評価されます。 だから私たちが this にデータを書き込むとき、それはこれらのオブジェクトに格納されます。

結果として、メソッドは共有されますが、オブジェクトの状態は共有されません。

サマリ

  • JavaScriptでは、すべてのオブジェクトは隠れた [[Prototype]] プロパティを持っており、それは別のオブジェクトまたは null です。
  • それにアクセスするために obj.__proto__ を使うことができます(他の方法もあります。それらは後ほど学びます)。
  • [[Prototype]] によるオブジェクトの参照は “プロトタイプ” と呼ばれます。
  • もしも obj のプロパティを読みたい、またはメソッドを呼び出したいが存在しない場合、JavaScriptはそれをプロトタイプの中で見つけようとします。書き込み/削除操作はオブジェクトに対して直接動作し、プロトタイプを使いません(プロパティが setter でない限り)。
  • もしも obj.method() を呼び出し、method がプロトタイプから取られた場合も、this は依然として obj を参照します。したがって、メソッドはたとえ継承されていたとしても、常に現在のオブジェクトで動作します。

タスク

重要性: 5

ここに2つのオブジェクトを作り、次にそれらを変更するコードがあります。

処理の中で、どの値が表示されるでしょう?

let animal = {
  jumps: null
};
let rabbit = {
  __proto__: animal,
  jumps: true
};

alert( rabbit.jumps ); // ? (1)

delete rabbit.jumps;

alert( rabbit.jumps ); // ? (2)

delete animal.jumps;

alert( rabbit.jumps ); // ? (3)

3つ答えてください。

  1. true です, rabbit から取られます.
  2. null です, animal から取られます.
  3. undefined です, このようなプロパティはもやは存在しません。
重要性: 5

このタスクは2つのパートを持っています。

オブジェクトがあります:

let head = {
  glasses: 1
};

let table = {
  pen: 3
};

let bed = {
  sheet: 1,
  pillow: 2
};

let pockets = {
  money: 2000
};
  1. __proto__ を使って、プロパティの参照が次のパスに従うようプロトタイプを割り当てます: pocketsbedtablehead. 例えば、pockets.pen3 (table にある), で bed.glasses1 (head にある)です。
  2. 質問に答えてください: glasses を取得するのに pocket.glasses がより速いですか?それとも head.glasses でしょうか?必要に応じてベンチマークしてください。
  1. __proto__ を追加してみましょう:

    let head = {
      glasses: 1
    };
    
    let table = {
      pen: 3,
      __proto__: head
    };
    
    let bed = {
      sheet: 1,
      pillow: 2,
      __proto__: table
    };
    
    let pockets = {
      money: 2000,
      __proto__: bed
    };
    
    alert( pockets.pen ); // 3
    alert( bed.glasses ); // 1
    alert( table.money ); // undefined
  2. 現代のエンジンにおいて、パフォーマンス面ではオブジェクトもしくはそのプロトタイプからプロパティを取得するかどうかの違いはありません。プロパティが見つかった場所を覚えており、次の要求で再利用します。

    例えば、pockets.glasses では、glasses (head の中)がどこにあるのかを覚えていて、次回そこをすぐに探します。また、何か変更があった場合に内部キャッシュを更新するには十分賢いので、最適化は安全です。

重要性: 5

animal から継承している rabbit があります。

もし rabbit.eat() を呼び出す場合、どのオブジェクトが full を受け取りますか?: animal または rabbit?

let animal = {
  eat() {
    this.full = true;
  }
};

let rabbit = {
  __proto__: animal
};

rabbit.eat();

解答: rabbit.

this はドットの前のオブジェクトなので、 rabbit.eat()rabbit を変更します。

プロパティの参照と実行は2つの異なるものです。 メソッド rabbit.eat は最初にプロトタイプで見つけられ、this=rabbit で実行されます。

重要性: 5

私たちは2匹のハムスターを持っています: speedylazy は一般的な hamster オブジェクトを継承しています。

そのうちの1匹に餌をやるとき、もう1匹もお腹一杯になります。なぜでしょう?どのような直しますか?

let hamster = {
  stomach: [],

  eat(food) {
    this.stomach.push(food);
  }
};

let speedy = {
  __proto__: hamster
};

let lazy = {
  __proto__: hamster
};

// 一方が食べ物を見つけました
speedy.eat("apple");
alert( speedy.stomach ); // apple

// もう一方も持っています。なぜでしょう?直してください。
alert( lazy.stomach ); // apple

呼び出し speedy.eat("apple") の中で何が起きているか注意深く見ていきましょう。

  1. メソッド speedy.eat はプロトタイプ(=hamster) で見つかり、this=speedy (ドットの前のオブジェクト)で実行されます。

  2. 次に、this.stomach.push()stomach プロパティを見つけ、push する必要があります。それは this (=speedy) の中で stomach を探しますが、見つかりません。

  3. 次に、プロトタイプチェーンに従って、hamster の中で stomach を見つけます。

  4. そして、push を呼び出し、プロトタイプの stomach の中に食べ物を追加します。

従って、すべてのハムスターは1つの胃(stomach)を共有しています!

毎回、stomach はプロトタイプから取られ、stomach.push は “その場” で変更します。

シンプルな代入 this.stomach= の場合には、このようなことは起こらないことに注意してください。:

let hamster = {
  stomach: [],

  eat(food) {
    // this.stomach.push の代わりに this.stomach に代入する
    this.stomach = [food];
  }
};

let speedy = {
   __proto__: hamster
};

let lazy = {
  __proto__: hamster
};

// Speedy は食べ物を見つけました
speedy.eat("apple");
alert( speedy.stomach ); // apple

// Lazy の胃は空っぽです
alert( lazy.stomach ); // <nothing>

今、すべてうまく行きました。なぜなら this.stomach=stomach を参照しないからです。値は直接 this オブジェクトに書き込まれます。

また、各ハムスターが自身の胃をもつことで問題を避けることができます:

let hamster = {
  stomach: [],

  eat(food) {
    this.stomach.push(food);
  }
};

let speedy = {
  __proto__: hamster,
  stomach: []
};

let lazy = {
  __proto__: hamster,
  stomach: []
};

// Speedy one found the food
speedy.eat("apple");
alert( speedy.stomach ); // apple

// Lazy one's stomach is empty
alert( lazy.stomach ); // <nothing>

一般的な解決策として、上の stomach のような、特定のオブジェクトの状態を説明するすべてのプロパティは通常そのオブジェクトの中に書かれます。それはこのような問題を防ぎます。

チュートリアルマップ

コメント

コメントをする前に読んでください…
  • 自由に記事への追加や質問を投稿をしたり、それらに回答してください。
  • 数語のコードを挿入するには、<code> タグを使ってください。複数行の場合は <pre> を、10行を超える場合にはサンドボックスを使ってください(plnkr, JSBin, codepen…)。
  • 記事の中で理解できないことがあれば、詳しく説明してください。