JavaScript Primerのスポンサーを募集中

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

オブジェクト」の章では、オブジェクトの処理方法について見てきました。その中で、空のオブジェクトであっても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(typeofObject.prototype.toString);// => "function"

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

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

次のコードでは、オブジェクトリテラルで作成(インスタンス化)したオブジェクトから、Object.prototype.toStringメソッドを参照しています。このときに、インスタンスのtoStringメソッドとObject.prototype.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.prototype.toStringメソッドなどを呼び出せるようになっています。

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

[コラム]Object#toStringという短縮した表記について

この書籍では、Object.prototype.toStringのようにprototypeを含めて毎回書くと冗長なため、「ObjectのtoStringメソッド」と短縮して書く場合があります。この書籍以外の文章では、Object.prototype.toStringObject#toStringのようにprototypeの代わりに#を利用して表しているケースがあります。

#prototypeの短縮表現として使われていたのは、#がJavaScriptの構文として使われていない記号でもあったためです。詳細は「クラス」の章で解説しますが、ES2022では#がJavaScriptの構文として追加され、#という記号が意味をもつようになりました。ES2022以降では、説明のために#prototypeの短縮表現に使うと、人によっては異なる意味に見えてしまう可能性があります。

そのため、この書籍はObject.prototype.toStringObject#toStringのように#を使って表す短縮表記は利用していません。

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

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

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

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

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

Object.hasOwn静的メソッドとin演算子との違い

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

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

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

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

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

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

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

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

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

// const obj = {} と同じ意味const obj =Object.create(Object.prototype);// `obj`は`Object.prototype`を継承している// そのため、`obj.toString`と`Object.prototype.toString`は同じとなるconsole.log(obj.toString ===Object.prototype.toString);// => true

ArrayもObjectを継承している

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

Arrayのインスタンス →Array.prototypeObject.prototype

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

// このコードはイメージです!// `Array`コンストラクタ自身は関数でもあるconstArray =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.prototype.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.prototype.toStringメソッドもそのひとつです。そのため、ArrayのインスタンスでtoStringメソッドを呼び出すとArray.prototype.toStringが優先して呼び出されます。

const numbers = [1,2,3];// `Array.prototype.toString`が定義されているため、`Object.prototype.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」の章で詳しく紹介します。

またObject.create(null)によって作成される空のオブジェクトは、Object.hasOwn静的メソッドがES2022で導入された理由でもあります。

次のように、Object.prototypeを継承しないオブジェクトは、Object.prototype.hasOwnPropertyメソッドを呼び出せません。そのため、オブジェクトがプロパティを持っているかということを確認する際に、単純にはhasOwnPropertyメソッドが使えないという状況が出てきました。

// Mapのような空オブジェクトconst mapLike =Object.create(null);// `Object.prototype`を継承していないため呼び出すと例外が発生するconsole.log(mapLike.hasOwnProperty("key"));// => Error: hasOwnPropertyメソッドは呼び出せない

ES2022から導入されたObject.hasOwn静的メソッドは、対象のオブジェクトがObject.prototypeを継承していないかは関係なく利用できます。

// Mapのような空オブジェクトconst mapLike =Object.create(null);// keyは存在しないconsole.log(Object.hasOwn(mapLike,"key"));// => false

このように、対象となるオブジェクトに依存しないObject.hasOwn静的メソッドは、hasOwnPropertyメソッドの欠点を修正しています。

まとめ

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

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

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