2022年5月6日

プロトタイプ継承

プログラミングでは、しばしば何かを拡張したい場合があります。

例えば、プロパティとメソッドを持つuserオブジェクトがあり、それを少し変更したバリアントとしてadminguestを作成したいとします。 userにあるものをコピー/再実装するのではなく、再利用して、その上に新しいオブジェクトを構築したいと考えています。

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

[[Prototype]]

JavaScriptでは、オブジェクトには特別な非表示プロパティ[[Prototype]](仕様書に記載されている名前)があり、それはnullか別のオブジェクトを参照しています。そのオブジェクトは「プロトタイプ」と呼ばれます。

objectからプロパティを読み込んで、それが存在しない場合、JavaScriptは自動的にプロトタイプから取得します。プログラミングでは、これは「プロトタイプ継承」と呼ばれます。そしてすぐに、そのような継承の多くの例と、それに基づいて構築されたよりクールな言語機能を学習します。

プロパティ[[Prototype]]は内部的で非表示ですが、それを設定する方法はたくさんあります。

そのうちの1つは、次のように特別な名前__proto__を使用することです。

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

rabbit.__proto__ = animal; // sets rabbit.[[Prototype]] = animal

これで、rabbitからプロパティを読み込んで、それが存在しない場合、JavaScriptは自動的にanimalから取得します。

例えば

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

rabbit.__proto__ = animal; // (*)

// we can find both properties in rabbit now:
alert( rabbit.eats ); // true (**)
alert( rabbit.jumps ); // true

ここで、行(*)animalrabbitのプロトタイプとして設定します。

次に、alertがプロパティrabbit.eats (**)を読み取ろうとすると、それはrabbitにないため、JavaScriptは[[Prototype]]参照をたどり、animalで見つけます(下から上を見てください)。

ここでは、「animalrabbitのプロトタイプです」または「rabbitanimalからプロトタイプ的に継承します」と言うことができます。

そのため、animalに多くの便利なプロパティとメソッドがある場合、それらはrabbitで自動的に利用可能になります。このようなプロパティは「継承された」と呼ばれます。

animalにメソッドがある場合、それをrabbitで呼び出すことができます。

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

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

// walk is taken from the 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 is taken from the prototype chain
longEar.walk(); // Animal walk
alert(longEar.jumps); // true (from rabbit)

これで、longEarから何かを読み込んで、それが存在しない場合、JavaScriptはそれをrabbitで探し、次にanimalで探します。

制限は2つだけです。

  1. 参照は循環できません。 __proto__を循環して割り当てようとすると、JavaScriptはエラーをスローします。
  2. __proto__の値は、オブジェクトまたはnullのいずれかです。他のタイプは無視されます。

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

__proto__[[Prototype]]の歴史的なゲッター/セッターです。

初心者の開発者がこの2つの違いを知らないのはよくある間違いです。

__proto__は内部の[[Prototype]]プロパティと_同じではない_ことに注意してください。これは、[[Prototype]]のゲッター/セッターです。後でそれが重要な状況を確認しますが、今はJavaScript言語の理解を深めるにつれて、それを覚えておきましょう。

__proto__プロパティは少し古くなっています。歴史的な理由で存在しますが、最新のJavaScriptでは、プロトタイプを取得/設定するObject.getPrototypeOf / Object.setPrototypeOf関数を使用することをお勧めします。これらの関数についても後で説明します。

仕様では、__proto__はブラウザでのみサポートされる必要があります。しかし実際には、サーバーサイドを含むすべての環境で__proto__がサポートされているため、使用しても安全です。

__proto__表記の方が直感的にわかりやすいため、例ではそれを使用します。

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

プロトタイプはプロパティの読み取りにのみ使用されます.

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

以下の例では、独自のwalkメソッドをrabbitに割り当てています.

let animal = {
  eats: true,
  walk() {
    /* this method won't be used by rabbit */
  }
};

let rabbit = {
  __proto__: animal
};

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

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

これ以降、rabbit.walk()呼び出しは、プロトタイプを使用せずに、オブジェクト内のメソッドをすぐに見つけて実行します.

アクセサプロパティは例外です。割り当てはセッター関数によって処理されるためです。そのため、そのようなプロパティへの書き込みは、実際には関数を呼び出すのと同じです.

そのため、以下のコードでは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 triggers!
admin.fullName = "Alice Cooper"; // (**)

alert(admin.fullName); // Alice Cooper, state of admin modified
alert(user.fullName); // John Smith, state of user protected

ここで、行(*)のプロパティadmin.fullNameにはプロトタイプuserにゲッターがあるため、それが呼び出されます。そして、行(**)のプロパティにはプロトタイプにセッターがあるため、それが呼び出されます.

「this」の値

上記の例では、興味深い質問が生じる可能性があります。set fullName(value)内の`this`の値は何ですか?プロパティ`this.name`と`this.surname`はどこに書き込まれますか? userまたはadminに?

答えは簡単です。`this`はプロトタイプの影響をまったく受けません.

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

そのため、セッター呼び出しadmin.fullName =は、userではなくadminを`this`として使用します.

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

たとえば、ここではanimalは「メソッドストレージ」を表し、rabbitはそれを使用します。

呼び出しrabbit.sleep()は、rabbitオブジェクトにthis.isSleepingを設定します。

// animal has methods
let animal = {
  walk() {
    if (!this.isSleeping) {
      alert(`I walk`);
    }
  },
  sleep() {
    this.isSleeping = true;
  }
};

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

// modifies rabbit.isSleeping
rabbit.sleep();

alert(rabbit.isSleeping); // true
alert(animal.isSleeping); // undefined (no such property in the prototype)

結果の図

animalから継承するbirdsnakeなどの他のオブジェクトがある場合、それらもanimalのメソッドにアクセスできます。ただし、各メソッド呼び出しの`this`は、呼び出し時(ドットの前)に評価された対応するオブジェクトであり、animalではありません。そのため、`this`にデータを書き込むと、これらのオブジェクトに格納されます。

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

for…inループ

for..inループは、継承されたプロパティも反復処理します.

例えば

let animal = {
  eats: true
};

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

// Object.keys only returns own keys
alert(Object.keys(rabbit)); // jumps

// for..in loops over both own and inherited keys
for(let prop in rabbit) alert(prop); // jumps, then 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があります。

面白いことが1つあります。メソッドrabbit.hasOwnPropertyはどこから来ているのでしょうか?定義していません。チェーンを見ると、メソッドはObject.prototype.hasOwnPropertyによって提供されていることがわかります。言い換えれば、それは継承されています。

…しかし、for..inが継承されたプロパティをリストする場合、hasOwnPropertyeatsjumpsのようにfor..inループに表示されないのはなぜでしょうか?

答えは簡単です。列挙可能ではありません。 Object.prototypeの他のすべてのプロパティと同様に、enumerable:falseフラグがあります。そして、for..inは列挙可能なプロパティのみをリストします。そのため、それおよび残りのObject.prototypeプロパティはリストされません.

他のほとんどすべてのキー/値取得メソッドは、継承されたプロパティを無視します.

Object.keysObject.valuesなどの他のほとんどすべてのキー/値取得メソッドは、継承されたプロパティを無視します.

それらはオブジェクト自体でのみ動作します。プロトタイプのプロパティは考慮され_ません_。

まとめ

  • JavaScriptでは、すべてのオブジェクトには、別のオブジェクトまたはnullである非表示の[[Prototype]]プロパティがあります.
  • obj.__proto__を使用してアクセスできます(歴史的なゲッター/セッター、他にも方法があります。近日中に説明します)。
  • [[Prototype]]によって参照されるオブジェクトは「プロトタイプ」と呼ばれます。
  • objのプロパティを読み取るかメソッドを呼び出したいが、それが存在しない場合、JavaScriptはプロトタイプでそれを見つけようとします。
  • 書き込み/削除操作はオブジェクトに直接作用し、プロトタイプを使用しません(セッターではなくデータプロパティであると仮定)。
  • obj.method()を呼び出し、methodがプロトタイプから取得された場合、`this`はまだ`obj`を参照します。そのため、メソッドは継承されていても常に現在のオブジェクトで動作します。
  • for..inループは、独自の プロパティと継承されたプロパティの両方を反復処理します。他のすべてのキー/値取得メソッドは、オブジェクト自体でのみ動作します.

タスク

重要度:5

オブジェクトのペアを作成してから変更するコードを次に示します.

プロセスでどの値が表示されますか?

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. truerabbitから取得。
  2. nullanimalから取得。
  3. undefined、そのようなプロパティはもうありません。
重要度:5

タスクには2つの部分があります.

次のオブジェクトが与えられた場合

let head = {
  glasses: 1
};

let table = {
  pen: 3
};

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

let pockets = {
  money: 2000
};
  1. プロパティルックアップがパスpocketsbedtableheadに従うように、__proto__を使用してプロトタイプを割り当てます。たとえば、pockets.pen3tableにあります)、bed.glasses1headにあります)である必要があります。
  2. 質問に答えてください。glassespockets.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 を変更するからです。

プロパティのルックアップと実行は別物です。

メソッド rabbit.eat は、最初にプロトタイプで見つかり、次に this=rabbit で実行されます。

重要度:5

一般的な hamster オブジェクトを継承する 2 匹のハムスター、speedylazy がいます。

一方に餌を与えると、もう一方も満腹になります。なぜでしょうか?どうすれば修正できるでしょうか?

let hamster = {
  stomach: [],

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

let speedy = {
  __proto__: hamster
};

let lazy = {
  __proto__: hamster
};

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

// This one also has it, why? fix please.
alert( lazy.stomach ); // apple

speedy.eat("apple") の呼び出しで何が起こっているのか詳しく見てみましょう。

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

  2. 次に、this.stomach.push()stomach プロパティを見つけて、push を呼び出す必要があります。 this (=speedy) で stomach を探しますが、見つかりません。

  3. 次に、プロトタイプチェーンをたどり、hamsterstomach を見つけます。

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

そのため、すべてのハムスターが 1 つの stomach を共有しています!

lazy.stomach.push(...)speedy.stomach.push() の両方で、プロパティ stomach はプロトタイプで見つかり (オブジェクト自体にはないため)、新しいデータがプッシュされます。

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

let hamster = {
  stomach: [],

  eat(food) {
    // assign to this.stomach instead of this.stomach.push
    this.stomach = [food];
  }
};

let speedy = {
   __proto__: hamster
};

let lazy = {
  __proto__: hamster
};

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

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

これで正常に動作します。this.stomach=stomach のルックアップを実行しないためです。値は this オブジェクトに直接書き込まれます。

また、各ハムスターが独自の stomach を持つようにすることで、問題を完全に回避できます。

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 のように、特定のオブジェクトの状態を表すすべてのプロパティは、そのオブジェクトに書き込む必要があります。 これにより、このような問題を防ぐことができます。

チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善のための提案がある場合は、コメントする代わりに、GitHub issue またはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • 短いコードを挿入するには <code> タグを使用し、複数行の場合は <pre> タグで囲み、10 行を超える場合はサンドボックス (plnkrjsbincodepen…) を使用してください。