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

オブジェクト」の章では、オブジェクトの処理方法について見ていきました。 その中で、空のオブジェクトであってもtoStringメソッドなどを呼びだせていました。

const obj = {};
console.log(obj.toString()); // "[object Object]"

オブジェクトリテラルで空のオブジェクトを定義しただけなのに、toStringメソッドを呼び出せています。 このメソッドはどこに実装されているのでしょうか?

また、JavaScriptにはtoString以外にも、オブジェクトに自動的に実装されるメソッドがあります。 これらのオブジェクトに組み込まれたメソッドをビルトインメソッドと呼びます。

この章では、これらのビルトインメソッドがどこに実装され、なぜObjectのインスタンスから呼び出せるのかを確認していきます。 詳しい仕組みについては「クラス」の章で改めて解説するため、この章では大まかな動作の流れを理解することが目的です。

Objectはすべての元

Objectには、他のArrayStringFunctionなどの他のオブジェクトとは異なる特徴があります。 それは、他のオブジェクトはすべてObjectを継承しているという点です。

正確には、ほとんどすべてのオブジェクトはObject.prototypeプロパティに定義されたprototypeオブジェクトを継承しています。 prototypeオブジェクトとは、すべてのオブジェクトの作成時に自動的に追加される特殊なオブジェクトです。 Objectprototypeオブジェクトは、すべてのオブジェクトから利用できるメソッドなどを提供するベースオブジェクトともいえます。

すべてのオブジェクトは`Object`の`prototype`を継承している

具体的にどういうことかを見てみます。

先ほども登場したtoStringメソッドは、Objectprototypeオブジェクトに定義があります。 次のように、Object.prototype.toStringメソッドの実装自体も参照できます。

// `Object.prototype`オブジェクトに`toString`メソッドの定義がある
console.log(typeof Object.prototype.toString); // => "function"

このようなprototypeオブジェクトに組み込まれているメソッドはプロトタイプメソッドと呼ばれます。 この書籍ではObject.prototype.toStringのようなプロトタイプメソッドをObject#toStringと短縮して表記します。

Object.prototype.toString = Object#toString

Objectのインスタンスは、このObject.prototypeオブジェクトに定義されたメソッドやプロパティをインスタンス化時に継承します。 つまり、オブジェクトリテラルやnew Objectでインスタンス化したオブジェクトは、Object.prototypeに定義されたものが利用できるということです。

次のコードでは、オブジェクトリテラルで作成(インスタンス化)したオブジェクトから、Object#toStringメソッドを参照しています。 このときに、インスタンスのtoStringメソッドとObject#toStringは同じものとなることがわかります。

const obj = {
    "key": "value"
};
// `obj`インスタンスは`Object.prototype`に定義されたものを継承する
// `obj.toString`は継承した`Object.prototype.toString`を参照している
console.log(obj.toString === Object.prototype.toString); // => true
// インスタンスからプロトタイプメソッドを呼び出せる
console.log(obj.toString()); // => "[object Object]"

このようにObject.prototypeに定義されているtoStringメソッドなどは、インスタンス作成時に自動的に継承されるため、Objectのインスタンスから呼び出せます。 これによりオブジェクトリテラルで作成した空のオブジェクトでも、Object#toStringメソッドなどを呼び出せるようになっています。

このインスタンスからprototypeオブジェクト上に定義されたメソッドを参照できる仕組みをプロトタイプチェーンと呼びます。 プロトタイプチェーンの仕組みについては「クラス」の章で扱うため、ここではインスタンスからプロトタイプメソッドを呼び出せるということがわかっていれば問題ありません。

プロトタイプメソッドとインスタンスメソッドの優先順位

プロトタイプメソッドと同じ名前のメソッドがインスタンスオブジェクトに定義されている場合もあります。 その場合には、インスタンスに定義したメソッドが優先して呼び出されます。

次のコードでは、ObjectのインスタンスであるcustomObjecttoStringメソッドを定義しています。 実行してみると、プロトタイプメソッドよりも優先してインスタンスのメソッドが呼び出されていることがわかります。

// オブジェクトのインスタンスにtoStringメソッドを定義
const customObject = {
    toString() {
        return "custom value";
    }
};
console.log(customObject.toString()); // => "custom value"

このように、インスタンスとプロトタイプオブジェクトで同じ名前のメソッドがある場合には、インスタンスのメソッドが優先されます。

in演算子とObject#hasOwnPropertyメソッドの違い

オブジェクト」の章で学んだObject#hasOwnPropertyメソッドとin演算子の挙動の違いについて見ていきます。 2つの挙動の違いはこの章で紹介したプロトタイプオブジェクトに関係しています。

hasOwnPropertyメソッドは、そのオブジェクト自身が指定したプロパティを持っているかを判定します。 一方、in演算子はオブジェクト自身が持っていなければ、そのオブジェクトの継承元であるprototypeオブジェクトまで探索して持っているかを判定します。 つまり、in演算子はインスタンスに実装されたメソッドなのか、プロトタイプオブジェクトに実装されたメソッドなのかは区別しません。

次のコードでは、空のオブジェクトがtoStringメソッドを持っているかをObject#hasOwnPropertyメソッドとin演算子でそれぞれ判定しています。 hasOwnPropertyメソッドはfalseを返し、in演算子はtoStringメソッドがプロトタイプオブジェクトに存在するためtrueを返します。

const obj = {};
// `obj`というオブジェクト自体に`toString`メソッドが定義されているわけではない
console.log(obj.hasOwnProperty("toString")); // => false
// `in`演算子は指定されたプロパティ名が見つかるまで親を辿るため、`Object.prototype`まで見に行く
console.log("toString" in obj); // => true

次のように、インスタンスがtoStringメソッドを持っている場合は、hasOwnPropertyメソッドもtrueを返します。

// オブジェクトのインスタンスにtoStringメソッドを定義
const obj = {
    toString() {
        return "custom value";
    }
};
// オブジェクトのインスタンスが`toString`メソッドを持っている
console.log(obj.hasOwnProperty("toString")); // => true
console.log("toString" in obj); // => true

これによりObjectのインスタンス自身がtoStringメソッドを持っているわけではなく、Object.prototypetoStringメソッドを持っていることが分かります。

オブジェクトの継承元を明示するObject.createメソッド

Object.createメソッドを使うと、第一引数に指定したprototypeオブジェクトを継承した新しいオブジェクトを作成できます。

先ほど、オブジェクトリテラルはObject.prototypeオブジェクトを自動的に継承したオブジェクトを作成していることがわかりました。 オブジェクトリテラルで作成する新しいオブジェクトは、Object.createメソッドを使うことで次のように書けます。

// const obj = {} と同じ意味
const obj = Object.create(Object.prototype);
// `obj`は`Object.prototype`を継承している
console.log(obj.hasOwnProperty === Object.prototype.hasOwnProperty); // => true

ArrayもObjectを継承している

ObjectObject.prototypeの関係と同じように、ビルトインオブジェクトArrayArray.prototypeを持っています。 同じように、配列(Array)のインスタンスはArray.prototypeを継承します。 さらに、Array.prototypeObject.prototypeを継承しているため、ArrayのインスタンスはObject.prototypeも継承してます。

Arrayのインスタンス -> Array.prototype -> Object.prototype

Object.createメソッドを使ってArrayObjectの関係をコードとして表現してみます。 この擬似コードは、Arrayコンストラクタの実装などは実際のものとは異なる部分があるため、あくまでイメージであることに注意してください。

// このコードはイメージです!
// `Array`コンストラクタ自身は関数でもある
const Array = function() {};
// `Array.prototype`は`Object.prototype`を継承している
Array.prototype = Object.create(Object.prototype);
// `Array`のインスタンスは、`Array.prototype`を継承している
const array = Object.create(Array.prototype);
// `array`は`Object.prototype`を継承している
console.log(array.hasOwnProperty === Object.prototype.hasOwnProperty); // => true

このように、ArrayのインスタンスもObject.prototypeを継承しているため、 Object.prototypeに定義されているメソッドを利用できます。

次のコードでは、ArrayのインスタンスからObject#hasOwnPropertyメソッドが参照できていることがわかります。

const array = [];
// `Array`のインスタンス -> `Array.prototype` -> `Object.prototype`
console.log(array.hasOwnProperty === Object.prototype.hasOwnProperty); // => true

このようなhasOwnPropertyメソッドの参照が可能なのもプロトタイプチェーンという仕組みによるものです。

ここでは、Object.prototypeはすべてのオブジェクトの親となるオブジェクトであることだけを覚えておくだけで問題ありません。 これにより、ArrayStringなどのインスタンスもObject.prototypeがもつメソッドを利用できる点を覚えておきましょう。

また、Array.prototypeなどもそれぞれ独自のメソッドを定義しています。 たとえば、Array#toStringメソッドもそのひとつです。 そのため、配列のインスタンスでtoStringメソッドを呼び出すとArray#toStringが優先して呼び出されます。

const numbers = [1, 2, 3];
// `Array#toString`が定義されているため、`Object#toString`とは異なる形式となる
console.log(numbers.toString()); // => "1,2,3"

[コラム] Object.prototypeを継承しないオブジェクト

Objectはすべてのオブジェクトの親になるオブジェクトであると言いましたが、例外もあります。

イディオム(慣習的な書き方)ですが、Object.create(null)とすることでObject.prototypeを継承しないオブジェクトを作成できます。 これにより、プロパティやメソッドを全く持たない本当に空のオブジェクトを作ることができます。

// 親がnull、つまり親がいないオブジェクトを作る
const obj = Object.create(null);
// Object.prototypeを継承しないため、hasOwnPropertyが存在しない
console.log(obj.hasOwnProperty); // => undefined

Object.createメソッドはES5から導入されました。 Object.createメソッドはObject.create(null)というイディオムで、一部ライブラリなどでMapオブジェクトの代わりとして利用されていました。 Mapとはキーと値の組み合わせを保持するためのオブジェクトです。

ただのオブジェクトもMapとよく似た性質を持っていますが、最初からいくつかのプロパティが存在しアクセスできてしまいます。 なぜなら、ObjectのインスタンスはデフォルトでObject.prototypeを継承するため、toStringなどのプロパティ名がオブジェクトを作成した時点で存在するためです。そのため、Object.create(null)Object.prototypeを継承しないオブジェクトを作成し、そのオブジェクトがMapの代わりとして使われていました。

// 空オブジェクトを作成
const obj = {};
// "toString"という値を定義してないのに、"toString"が存在している
console.log(obj["toString"]);// Function 
// Mapのような空オブジェクト
const mapLike = Object.create(null);
// toStringキーは存在しない
console.log(mapLike["toString"]); // => undefined

しかし、ES2015からは、本物のMapが利用できるため、Object.create(null)Mapの代わりに利用する必要はありません。 Mapについては「Map/Set」の章で詳しく紹介します。

const map = new Map();
// toStringキーは存在しない
console.log(map.has("toString")); // => false

まとめ

この章では、プロトタイプオブジェクトについて学びました。

  • プロトタイプオブジェクトはオブジェクトの作成時に自動的に作成される
  • ObjectのプロトタイプオブジェクトにはtoStringなどのプロトタイプメソッドが定義されている
  • ほとんどのオブジェクトはObject.prototypeを継承することでtoStringメソッドなどを呼び出せる
  • プロトタイプメソッドとインスタンスメソッドではインスタンスメソッドが優先される
  • Object.createメソッドを使うことでプロトタイプオブジェクトを継承しないオブジェクトを作成できる

プロトタイプオブジェクトに定義されているメソッドがどのように参照されているかを確認しました。 このプロトタイプの詳しい仕組みについては「クラス」の章で改めて解説します。