第16章 プロトタイプチェーン

プロトタイプの基本概念

JavaScriptはプロトタイプベースの言語であり、その核となる特徴の1つが「プロトタイプ」です。この概念を理解するには以下のポイントに注意してください。

  1. オブジェクトの関連性
  2. JavaScriptのオブジェクトは、他のオブジェクトに「リンク」しており、この「リンク」の先が「プロトタイプ」です。このリンクによって、あるオブジェクトが持っていないプロパティやメソッドにアクセスしようとした場合、JavaScriptはプロトタイプ内を検索します。
  3. プロトタイプチェーン
  4. オブジェクトがそのプロトタイプの中でも該当するプロパティやメソッドを持っていない場合、JavaScriptはさらにそのプロトタイプのプロトタイプを検索します。このように連鎖的にオブジェクトのプロトタイプを辿ることを「プロトタイプチェーン」と呼びます。
  5. 関数とprototypeプロパティ
  6. JavaScriptにおける全ての関数はprototypeというプロパティを持っています。このプロパティはオブジェクトを指し、新しく作成されるインスタンスのプロトタイプとして利用されます。

実際のコード例

function Person(name) {
    this.name = name;
}

// Personのprototypeにメソッドを追加
Person.prototype.introduce = function() {
    console.log(`私の名前は${this.name}です。`);
};

const taro = new Person('太郎');
taro.introduce(); // "私の名前は太郎です。" と表示される

console.log(taro.hasOwnProperty('name'));       // true
console.log(taro.hasOwnProperty('introduce')); // false

この例では、introduceメソッドはPersonのプロトタイプに追加されています。それゆえ、taroオブジェクトは直接introduceメソッドを持っていませんが、そのプロトタイプチェーンを通じてintroduceメソッドにアクセスすることができます。

このように、プロトタイプを利用することで、メモリの効率的な利用やメソッドの再利用などの利点が得られます。


プロトタイプチェーン

プロトタイプチェーンは、JavaScriptの核心的な概念で、オブジェクト指向の継承と密接に関連しています。簡単に言えば、オブジェクトの特定のプロパティやメソッドにアクセスしようとすると、JavaScriptは次の順序でそのプロパティやメソッドを探します。

  1. 当該オブジェクト自体
  2. そのオブジェクトのプロトタイプ
  3. そのプロトタイプのプロトタイプ... という具合に

この連鎖的な検索を「プロトタイプチェーン」と呼びます。

なぜプロトタイプチェーンが必要か?

JavaScriptのオブジェクトは他のオブジェクトを「継承」することができます。継承したオブジェクトは、元のオブジェクトのプロパティやメソッドにアクセスする能力を持っています。この継承のメカニズムは、プロトタイプチェーンを通じて実現されています。

// コンストラクタ関数
function Animal(name) {
    this.name = name;
}

// Animalのprototypeにメソッドを追加
Animal.prototype.speak = function() {
    console.log(`${this.name} は音を出す`);
};

function Dog(name) {
    Animal.call(this, name); // Animalのコンストラクタを呼ぶ
}

// DogをAnimalから継承させる
Dog.prototype = Object.create(Animal.prototype);

// Dogのprototypeにメソッドを追加
Dog.prototype.bark = function() {
    console.log(`${this.name} はワンワンと吠える`);
};

const dog = new Dog('ポチ');
dog.speak(); // "ポチ は音を出す" と表示される
dog.bark();  // "ポチ はワンワンと吠える" と表示される

上記の例では、DogAnimalから継承しています。このため、dogオブジェクトはAnimalのプロトタイプにあるspeakメソッドにアクセスできます。また、Dogのプロトタイプに独自のメソッドbarkも追加されています。

このようなプロトタイプチェーンの仕組みによって、JavaScriptのオブジェクトは多重の継承やメソッドのオーバーライドなどの柔軟な動作を実現しています。


プロトタイプを利用した継承

JavaScriptでは、クラスベースの言語とは異なり、オブジェクトから直接オブジェクトを継承する「プロトタイプベースの継承」が主要な継承の仕組みとして存在しています。

1.基本的な継承

// 親クラスとしてのコンストラクタ関数
function Animal(name) {
    this.name = name;
}

Animal.prototype.speak = function() {
    console.log(`${this.name} は音を出す`);
};

// 子クラスとしてのコンストラクタ関数
function Dog(name) {
    Animal.call(this, name);  // 親クラスのコンストラクタを呼び出す
}

// DogのプロトタイプにAnimalのインスタンスをセットすることで継承を実現
Dog.prototype = new Animal();

const dog = new Dog('ポチ');
dog.speak();  // "ポチ は音を出す" と表示される

2.Object.create()を使った継承

上記の方法は、直感的で理解しやすい一方で、不必要なインスタンスを生成してしまう問題があります。これを避けるために、Object.create()を用いた方法が推奨されています。

function Dog(name) {
    Animal.call(this, name);
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;  // コンストラクタを正しく設定

const dog = new Dog('ハチ');
dog.speak();  // "ハチ は音を出す" と表示される

3.メソッドのオーバーライド

子クラスでは、親クラスのメソッドをオーバーライド(上書き)することが可能です。これにより、子クラスに特有の動作を実装できます。

Dog.prototype.speak = function() {
    console.log(`${this.name} はワンワンと吠える`);
};

const dog = new Dog('タロウ');
dog.speak();  // "タロウ はワンワンと吠える" と表示される


prototypeプロパティと__proto__プロパティの違い

もちろんです。「prototypeプロパティ」と「__proto__プロパティ」は、JavaScriptにおけるプロトタイプベースの継承を理解する上で中心的な役割を持つものですが、これらの機能や使い方は異なります。


prototypeプロパティ
  1. 何に使われるか: 主にコンストラクタ関数に対して使用されます。
  2. 目的: 新しく作成されるオブジェクトのプロトタイプオブジェクトを指定するために使用されます。
  3. 使用例:
    function MyConstructor() {}
    MyConstructor.prototype.someMethod = function() {
        // do something
    };
    

ここで、新しいインスタンスを生成するとき(例:new MyConstructor())、この新しいインスタンスのプロトタイプとしてMyConstructor.prototypeが使用されます。


proto プロパティ
  1. 何に使われるか: 任意のオブジェクトに対して使用できます。
  2. 目的: あるオブジェクトの実際のプロトタイプを指すものです(すなわち、メソッドやプロパティの検索時に、見つからなかった場合に次に参照されるオブジェクトを示します)。
  3. 使用例:
    const obj = {};
    console.log(obj.__proto__ === Object.prototype);  // true
    

この例では、空のオブジェクトの__proto__Object.prototypeを指しています。


その他の違い
  1. 変更の安全性
    • prototypeは主にコンストラクタ関数で使われ、新しいインスタンスを生成する際のプロトタイプとして機能します。
    • __proto__はオブジェクトの実際のプロトタイプを取得または設定するためのものですが、性能上の問題や予期しない振る舞いの原因となる可能性があるため、直接の変更は推奨されません。
  2. 標準性
    • prototypeはJavaScriptの初めから存在する正式なプロパティです。
    • __proto__は長らく非標準でありましたが、後にECMAScript 2015 (ES6) で標準化されました。しかしながら、使い方には注意が必要です。
  3. 存在
    • 全ての関数はprototypeプロパティを持っていますが、その他のオブジェクト(関数でないもの)は持っていません。
    • 一方で、ほとんどのオブジェクトは__proto__プロパティ(または内部[[Prototype]]スロット)を持っています。

このように、prototype__proto__は似て非なるものであり、それぞれの用途や背景を理解することで、JavaScriptのプロトタイプベースの継承の仕組みを深く掴むことができます。


プロトタイプの注意点と制限

プロトタイプとプロトタイプチェーンは非常に強力な機能を持つ一方で、適切に使用されないと思わぬ問題を引き起こすことがあります。以下に、プロトタイプに関する注意点と制限をいくつか挙げます。

プロトタイプ汚染(Prototype Pollution)
  1. オブジェクトのプロトタイプに直接プロパティやメソッドを追加することは、そのオブジェクトを継承する全てのオブジェクトに影響を及ぼします。これは予期しない挙動やバグを引き起こす原因となります。
  2. セキュリティ上のリスクも伴います。特に、外部からの入力を制御せずにプロトタイプを変更すると、攻撃者による意図的なプロトタイプ汚染のリスクが高まります。


プロトタイプチェーンの長さ
  1. プロトタイプチェーンが非常に長い場合、プロパティやメソッドの検索に時間がかかる可能性があり、パフォーマンスに影響を与える場合があります。
  2. また、プロトタイプチェーンが長いと、デバッグや保守が難しくなることがあります。


ネイティブプロトタイプの拡張
  1. Array.prototypeObject.prototypeなどのネイティブオブジェクトのプロトタイプを拡張するのは、多くの場面で避けるべきです。
  2. これにより、ネイティブオブジェクトが予期しない挙動をすることや、将来のJavaScriptのバージョンで同じ名前のメソッドやプロパティが追加された場合の衝突が発生する可能性があります。


__proto__の直接操作
  1. __proto__プロパティの直接操作は、ECMAScript 2015 (ES6) で標準化されましたが、その使用は推奨されません。
  2. Object.create()Object.getPrototypeOf()Object.setPrototypeOf()などのメソッドを使用して、プロトタイプの操作を行う方が安全です。


参照による継承
  1. JavaScriptのオブジェクトは参照によって渡されるため、プロトタイプを通じて継承されたプロパティがオブジェクトや配列の場合、そのプロパティを変更すると、全てのインスタンスで共有されることになります。


コンストラクタの問題
  1. プロトタイプを利用した継承を行う際に、コンストラクタの参照が壊れる問題が発生することがあります。これを解消するためには、コンストラクタの参照を手動で修正する必要があります。


練習問題1.

以下のPersonコンストラクタ関数とプロトタイプを使って、sayHelloメソッドを追加してください。このメソッドは、"Hello, my name is [名前]"というメッセージをコンソールに出力します。

function Person(name) {
  this.name = name;
}

// ここにsayHelloメソッドを追加

const person1 = new Person("Taro");
person1.sayHello();  // "Hello, my name is Taro" を出力


練習問題2.

次のコードが与えられます。

function Animal(name) {
  this.name = name;
}

Animal.prototype.eat = function() {
  console.log(this.name + ' is eating.');
};

function Dog(name) {
  Animal.call(this, name);
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {
  console.log(this.name + ' says woof!');
};

const dog1 = new Dog("Spot");

dog1オブジェクトを使用して、eatメソッドとbarkメソッドを呼び出して動作を確認してください。


練習問題3.

以下のVehicleというコンストラクタ関数が与えられます。このコンストラクタ関数を継承して、Carという新しいコンストラクタ関数を作成してください。CarVehicleの全てのプロパティとメソッドを継承し、さらにhonkメソッドを持ちます。このメソッドは、"Beep! Beep!"というメッセージをコンソールに出力します。

function Vehicle(type) {
  this.type = type;
}

Vehicle.prototype.drive = function() {
  console.log('Driving a ' + this.type);
};

// ここでCarコンストラクタ関数とプロトタイプを定義

const car1 = new Car("sedan");
car1.drive();       // "Driving a sedan" を出力
car1.honk();        // "Beep! Beep!" を出力