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

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

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

このtoStringメソッドなどはJavaScriptに組み込まれたビルトインメソッドですが、これらのメソッドはどこに実装されているのでしょうか? この章では、これらのビルトインメソッドがどのように実装され、なぜObjectのインスタンスから呼び出せるのかを見ていきます。

Objectはすべての元

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

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

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

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

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

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

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

Object.prototype.toString = Object#toString

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

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

// var object = new Object()も同じ
const object = {
    "key": "value"
};
// インスタンスがprototypeオブジェクトに定義されたものを継承する
// object.toString.prototype.toStringを参照している
console.log(object.toString === Object.prototype.toString); // => true
// インスタンスからプロトタイプメソッドを呼び出せる
console.log(object.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 object = {};
// `object`のインスタンス自体に`toString`メソッドが定義されているわけではない
console.log(object.hasOwnProperty("toString")); // => false
// `in`演算子は指定されたプロパティ名が見つかるまで親を辿るため、`Object.prototype`まで見に行く
console.log("toString" in object); // => true

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

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

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

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

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

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

// const object = {} と同じ
const object = Object.create(Object.prototype);
// `object`は`Object.prototype`を継承している
console.log(object.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

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

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

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

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

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

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

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

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

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

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

// ただのオブジェクト
const object = {};
// "toString"という値を定義してないのに、"toString"が存在している
console.log(object["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