2022年3月13日

プロトタイプ継承

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

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

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

プロトタイプ [[Prototype]]

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

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

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

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

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

rabbit.__proto__ = animal; // rabbit.[[Prototype]] = animal をセット

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 から)

Now if we read something from longEar, and it’s missing, JavaScript will look for it in rabbit, and then in animal.

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

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

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

__proto__[[Prototype]] のための歴史的な getter/setter です。

この2つの違いを知らないのは、初心者の開発者にありがちなミスです。

__proto__ は内部の [[Prototype]] プロパティとは 同じではない ことに注意してください。これは、[[Prototype]] のgetter/setter です。後でそれが重要な場面をみていきますが、今は JavaScript 言語を理解するために、このプロパティについて心に留めておいてください。

__proto__ プロパティは少し時代遅れです。これは歴史的な理由から存在しており、モダンな JavaScript では、プロトタイプの get/set の代わりに、 Object.getPrototypeOf/Object.setPrototypeOf 関数を使用することが推奨されています。これらの関数についても、後ほど説明していきます。

スペックでは、__proto__ はブラウザでのみサポートされなければなりません。ですが、実際にはサーバサイドを含むすべての環境で__proto__ がサポートされています。ので、安全に使用することができます。

__proto__ 記法は直感的でわかりやすいので、例ではこの記法を使います。

書き込みはプロトタイプを使用しません

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

書き込み/削除操作はオブジェクトで直接動作します。

下の例では、自身の 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"; // (**)

alert(admin.fullName); // Alice Cooper, admin の状態が変更されました。
alert(user.fullName); // John Smith, user の状態はそのままです。

ここで、行 (*) では、プロパティ 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 にデータを書き込むとき、これらのオブジェクトに格納されます。

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

for…in loop

for..in ループは継承したプロパティも繰り返し処理します。

例:

let animal = {
  eats: true
};

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

// Object.keys は実親のキーだけを返します
alert(Object.keys(rabbit)); // jumps

// for..in は実親と継承したキー両方をループします
for(let prop in rabbit) alert(prop); // jumps, eats

これは期待している動作ではなく、継承したプロパティは除きたい場合は組み込みのメソッド obj.hasOwnProperty(key)が利用できます。これは objkey という名前のプロパティをもつ場合(継承したものは含みません)、true を返します。

従って、継承したプロパティをフィルタする(あるいはそれらについて何かをする)ことが可能です。:

let animal = {
  eats: true
};

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

for(let prop in rabbit) {
  let isOwn = rabbit.hasOwnProperty(prop);

  if (isOwn) {
    alert(`Our: ${prop}`); // Our: jumps
  } else {
    alert(`Inherited: ${prop}`); // Inherited: eats
  }
}

ここでは、次の継承チェーンがあります: rabbitanimal を継承し、animalObject.prototype を継承しています(animal はリテラルオブジェクト {...} なので、これはデフォルトです)。その上は null です。:

イメージ "/article/prototype-inheritance/rabbit-animal-object.svg" が見つかりませんでした

興味深い点が1つあります。メソッド rabbit.hasOwnProperty はどこから来るのでしょうか?我々は定義していません。チェーンをみることで、メソッドは Object.prototype.hasOwnProperty で提供されていることがわかります。つまり、継承されたものです。

…ですが、なぜ hasOwnProperty は継承したプロパティもリストする for..in ループで eatsjumps のように登場しないのでしょう。

答えはシンプルです。これは列挙不可だからです。Object.prototype の他のすべてのプロパティと同様に、enumerable:false フラグを持ちます。そして、for..in は列挙可能なプロパティのみをリストします。そのため、Object.prototype のプロパティは列挙されません。

ほぼすべての他のキー/バリュー取得メソッドは継承したプロパティを無視します

Object.keys, Object.values などのほとんどの他のキー/バリュー取得メソッドは、継承したプロパティを無視します。

これらはオブジェクト自身の操作のみ行います。プロトタイプからのプロパティは考慮され ません

サマリ

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

タスク

重要性: 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…)。
  • 記事の中で理解できないことがあれば、詳しく説明してください。