オブジェクトのプロトタイプ

プロトタイプは、JavaScript オブジェクトが他のものから機能を継承する仕組みです。この記事では、プロトタイプとは何か、プロトタイプチェーンの動作方法、オブジェクトのプロトタイプを設定するにはどうすればよいかを解説していきます。

前提条件: JavaScript 関数の理解、JavaScript の基礎知識(JavaScript の第一歩JavaScript の構成要素を参照)、OOJS の基礎(オブジェクトの基本を参照)。
目標: JavaScript のオブジェクトのプロトタイプ、プロトタイプチェーンの動作方法、prototype プロパティに新しいメソッドを追加する方法を理解する。

プロトタイプチェーン

ブラウザーのコンソールで、オブジェクトリテラルを作成してみてください。

const myObject = {
  city: "Madrid",
  greet() {
    console.log(`Greetings from ${this.city}`);
  },
};

myObject.greet(); // Greetings from Madrid

これは、1 つのデータプロパティ city と 1 つのメソッド greet() を持つオブジェクトです。オブジェクトの名前の後にピリオドを付けて myObject. のようにコンソールに入力すると、このオブジェクトで利用できるすべてのプロパティのリストがポップアップ表示されます。citygreet だけでなく、他にもたくさんのプロパティがあることがわかると思います。

__defineGetter__
__defineSetter__
__lookupGetter__
__lookupSetter__
__proto__
city
constructor
greet
hasOwnProperty
isPrototypeOf
propertyIsEnumerable
toLocaleString
toString
valueOf

それらのうちの 1 つにアクセスしてみます。

myObject.toString(); // "[object Object]"

これはうまくいきます(たとえ toString() が何をするのかが明らかでないとしても)。

これらの追加のプロパティは何でしょうか、そしてそれはどこから来ているのでしょうか?

JavaScript ですべてのオブジェクトは、そのプロトタイプと呼ばれる組み込みプロパティを持っています。プロトタイプはそれ自体がオブジェクトなので、プロトタイプは自分自身でプロトタイプを持ち、プロトタイプチェーンと呼ばれるものを持ちます。自分自身でプロトタイプにnullを持つプロトタイプに到達すると、その連鎖は終わります。

メモ: オブジェクトのプロトタイプを指し示すプロパティは prototype という名前ではありません。その名前は標準ではありませんが、実際にはすべてのブラウザーが __proto__ を使用しています。オブジェクトのプロトタイプにアクセスする標準的な方法は Object.getPrototypeOf() メソッドです。

オブジェクトのプロパティにアクセスしようとしたとき、オブジェクト自身にプロパティが見つからない場合は、プロトタイプを検索してプロパティを探します。それでもプロパティが見つからない場合は、プロトタイプのプロトタイプが検索され、プロパティが得られるか、チェーンの終わりに達するか、その場合は undefined を返すまで、そのような具合に繰り返します。

つまり、myObject.toString() を呼び出すと、ブラウザーは、

  • toStringmyObject で探します。
  • 見つからない場合は、myObject のプロトタイプオブジェクトで toString を探します。
  • 見つかったら、それを呼び出します。

myObject のプロトタイプは何でしょうか?それを探すには、 Object.getPrototypeOf() 関数を使用することができます。

Object.getPrototypeOf(myObject); // Object { }

これは Object.prototype と呼ばれるオブジェクトで、既定ではすべてのオブジェクトが持つ、最も基本的なプロトタイプです。 Object.prototype のプロトタイプは null であり、プロトタイプの連鎖の終わりにあります。

myObject のプロトタイプチェーン

オブジェクトのプロトタイプは常に Object.prototype であるとは限りません。次のようになってみてください。

const myDate = new Date();
let object = myDate;

do {
  object = Object.getPrototypeOf(object);
  console.log(object);
} while (object);

// Date.prototype
// Object { }
// null

このコードでは、Date オブジェクトを作成し、プロトタイプの連鎖をたどって、プロトタイプをログ出力しています。このコードでは、myDate のプロトタイプは Date.prototype オブジェクトであり、その プロトタイプは Object.prototype であることを示します。

Prototype chain for myDate

実際、myDate2.getMonth()のような馴染みのあるメソッドを呼び出す場合、Date.prototypeで定義されたメソッドを呼び出していることになります。

プロパティのシャドウ化

オブジェクトにプロパティを定義した場合、そのオブジェクトのプロトタイプに同じ名前のプロパティが定義されているとどうなるのでしょうか。見てみましょう。

const myDate = new Date(1995, 11, 17);

console.log(myDate.getYear()); // 95

myDate.getYear = function () {
  console.log("something else!");
};

myDate.getYear(); // 'something else!'

これは、プロトタイプチェーンの説明から予想できることです。getYear() を呼び出すと、ブラウザーはまず myDate にその名前のプロパティがないか探し、myDate で定義されていない場合にのみプロトタイプを調べます。つまり、myDategetYear() を追加すると、myDate のバージョンが呼ばれることになります。

これはプロパティの「シャドウ化」と呼ばれます。

プロトタイプの設定

JavaScript でオブジェクトのプロトタイプを設定する様々な方法があります。ここでは Object.create() とコンストラクターの 2 つの方法について説明します。

Object.create の使用

Object.create() メソッドは、新しいオブジェクトを作成し、新しいオブジェクトのプロトタイプとして使用するオブジェクトを指定することができます。

以下に例を示します。

const personPrototype = {
  greet() {
    console.log("hello!");
  },
};

const carl = Object.create(personPrototype);
carl.greet(); // hello!

ここでは、greet() メソッドを持つオブジェクト personPrototype を作成しています。次に、Object.create() を使用して、personPrototype をプロトタイプとする新しいオブジェクトを作成します。これで、新しいオブジェクトに対して greet() を名付けることができ、プロトタイプはその実装を提供することになります。

コンストラクターの使用

JavaScript では、すべての関数が prototype という名前のプロパティを持っています。関数をコンストラクターとして呼び出すと、このプロパティが新しく構築されるオブジェクトのプロトタイプとして設定されます(規約では __proto__ というプロパティに設定されています)。

つまり、コンストラクターの prototype を設定すれば、そのコンストラクターで作成するすべてのオブジェクトに、確実にそのプロトタイプを持たせるすることができます。

const personPrototype = {
  greet() {
    console.log(`hello, my name is ${this.name}!`);
  },
};

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

Object.assign(Person.prototype, personPrototype);
// または
// Person.prototype.greet = personPrototype.greet;

ここでは次のものを生成します。

  • personPrototype オブジェクト。これは greet() メソッドを持っています
  • Person() コンストラクター関数。作成する人物の名前を初期化します。

次に、personPrototype で定義したメソッドを Person 関数の prototype プロパティに Object.assign で記述しています。

このコードの後、Person()を使用して作成したオブジェクトは、そのプロトタイプとして Person.prototype を取得し、その中に greet メソッドが自動的に格納されます。

const reuben = new Person("Reuben");
reuben.greet(); // hello, my name is Reuben!

これは、先ほど myDate のプロトタイプが Date.prototype と呼ばれる理由も説明します。これは Date コンストラクターの prototype プロパティです。

独自プロパティ

上記の Person コンストラクターを使用して作成したオブジェクトは、2 つのプロパティを持ちます。

  • name プロパティはコンストラクターで設定されるため、Person オブジェクトに直接現れます。
  • greet() メソッドは、プロトタイプに設定されています。

メソッドはプロトタイプで定義し、データプロパティはコンストラクターで定義する、というパターンはよく見かけます。これは、メソッドは作成するオブジェクトごとに同じものを使うのが普通ですが、データプロパティはオブジェクトごとに自分自身で値を示したい場合が多いからです(人ごとに名前が違うようのと似ています)。

ここでいう name のように、オブジェクトに直接定義するプロパティは独自プロパティと呼ばれ、あるプロパティが独自プロパティであるかどうかは静的メソッド Object.hasOwn() を使って調べることができます。

const irma = new Person("Irma");

console.log(Object.hasOwn(irma, "name")); // true
console.log(Object.hasOwn(irma, "greet")); // false

メモ: ここでは静的でない Object.hasOwnProperty() メソッドを用いることもできますが、できれば Object.hasOwn() を用いることをお勧めします。

プロトタイプと継承

プロトタイプは JavaScript の強力でとても柔軟な機能であり、コードの再利用やオブジェクトの結合を可能にします。

具体的には、プロトタイプは継承のバージョンに対応しています。継承とは、オブジェクト指向プログラミング言語の機能で、プログラマーが「システム内のあるオブジェクトは、他のオブジェクトのより特化したバージョンである」という考えを表現させることができます。

例えば、学校をモデル化する場合、教授と生徒がいるとします。これらはどちらも「人」であり、いくつかの特性(例えば、どちらも名前を持っている)を持っていますが、それぞれの例では追加する特性(教授は教える科目を持っている)があったり、同じ特性を異なる方法で実装したりするかもしれません。OOP システムでは、教授も学生も人から継承していると言います。

JavaScript で、ProfessorStudent のオブジェクトが Person プロトタイプを持つことができれば、共通のプロパティを継承することができる一方で、異なるプロパティを追加したり再定義したりしたりすることができることがおわかりいただけるでしょう。

次の記事では、オブジェクト指向プログラミング言語の他の主な機能とともに継承について説明し、JavaScript がどのように対応しているかを見ていくことにします。

まとめ

この記事では、プロトタイプオブジェクトチェーンによってオブジェクトが互いに機能を継承する方法、プロトタイププロパティとそれを使ってコンストラクタにメソッドを追加する方法など、JavaScript オブジェクトのプロトタイプを取り上げてきました。

次の記事では、オブジェクト指向プログラミングの基盤となる概念について見ていきます。