プロトタイプオブジェクト
「オブジェクト」の章では、オブジェクトの処理方法について見てきました。
その中で、空のオブジェクトであってもtoString
メソッドなどを呼び出せていました。
const obj = {};
console.log(obj.toString()); // "[object Object]"
オブジェクトリテラルで空のオブジェクトを定義しただけなのに、toString
メソッドを呼び出せています。
このメソッドはどこに実装されているのでしょうか?
また、JavaScriptにはtoString
以外にも、オブジェクトに自動的に実装されるメソッドがあります。
これらのオブジェクトに組み込まれたメソッドをビルトインメソッドと呼びます。
この章では、これらのビルトインメソッドがどこに実装され、なぜObject
のインスタンスから呼び出せるのかを確認していきます。
詳しい仕組みについては「クラス」の章で改めて解説するため、この章では大まかな動作の流れを理解することが目的です。
Object
はすべての元
Object
には、他のArray
、String
、Function
などのオブジェクトとは異なる特徴があります。
それは、他のオブジェクトはすべてObject
を継承しているという点です。
正確には、ほとんどすべてのオブジェクトはObject.prototype
プロパティに定義されたprototype
オブジェクトを継承しています。
prototype
オブジェクトとは、すべてのオブジェクトの作成時に自動的に追加される特殊なオブジェクトです。
Object
のprototype
オブジェクトは、すべてのオブジェクトから利用できるメソッドなどを提供するベースオブジェクトとも言えます。
具体的にどういうことかを見てみます。
先ほども登場したtoString
メソッドは、Object
のprototype
オブジェクトに定義があります。
次のように、Object.prototype.toString
メソッドの実装自体も参照できます。
// `Object.prototype`オブジェクトに`toString`メソッドの定義がある
console.log(typeof Object.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.toString
をObject#toString
のようにprototype
の代わりに#
を利用して表しているケースがあります。
#
がprototype
の短縮表現として使われていたのは、#
がJavaScriptの構文として使われていない記号でもあったためです。
詳細は「クラス」の章で解説しますが、ES2022では#
がJavaScriptの構文として追加され、#
という記号が意味をもつようになりました。
ES2022以降では、説明のために#
をprototype
の短縮表現に使うと、人によっては異なる意味に見えてしまう可能性があります。
そのため、この書籍はObject.prototype.toString
をObject#toString
のように#
を使って表す短縮表記は利用していません。
プロトタイプメソッドとインスタンスメソッドの優先順位
プロトタイプメソッドと同じ名前のメソッドがインスタンスオブジェクトに定義されている場合もあります。 その場合には、インスタンスに定義したメソッドが優先して呼び出されます。
次のコードでは、Object
のインスタンスであるcustomObject
にtoString
メソッドを定義しています。
実行してみると、プロトタイプメソッドよりも優先してインスタンスのメソッドが呼び出されていることがわかります。
// オブジェクトのインスタンスに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")); // => true
console.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を継承している
Object
とObject.prototype
の関係と同じように、ビルトインオブジェクトArray
もArray.prototype
を持っています。
同じように、配列(Array
)のインスタンスはArray.prototype
を継承します。
さらに、Array.prototype
はObject.prototype
を継承しているため、Array
のインスタンスはObject.prototype
も継承しています。
Array
のインスタンス →Array.prototype
→Object.prototype
Object.create
メソッドを使ってArray
とObject
の関係をコードとして表現してみます。
この疑似コードは、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.prototype.hasOwnProperty
メソッドが参照できていることがわかります。
const array = [];
// `Array`のインスタンス -> `Array.prototype` -> `Object.prototype`
console.log(array.hasOwnProperty === Object.prototype.hasOwnProperty); // => true
このようなhasOwnProperty
メソッドの参照が可能なのもプロトタイプチェーンという仕組みによるものです。
ここでは、Object.prototype
はすべてのオブジェクトの親となるオブジェクトであることを覚えておくだけで問題ありません。
これにより、Array
やString
などのインスタンスも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
メソッドを使うことでプロトタイプオブジェクトを継承しないオブジェクトを作成できる
プロトタイプオブジェクトに定義されているメソッドがどのように参照されているかを確認しました。 このプロトタイプの詳しい仕組みについては「クラス」の章で改めて解説します。