Object のプロトタイプ
プロトタイプは、JavaScript オブジェクトが互いに機能を継承するメカニズムです。この記事では、プロトタイプチェーンの仕組みを説明し、prototype
プロパティを使って既存のコンストラクタにメソッドを追加する方法を見ていきます。
メモ: この記事では、伝統的な JavaScript のコンストラクタとクラスを取り上げます。次の記事では、同じことを実現するためのより簡単な構文を提供する現代的な方法について話します - ECMAScript 2015 のクラスを参照してください。
前提条件: | JavaScript 関数の理解、JavaScript の基礎知識 (JavaScript の第一歩とJavaScript の構成要素を参照)、OOJS の基礎 (JavaScript オブジェクトの基本を参照) |
---|---|
目的: | JavaScript のオブジェクトのプロトタイプ、プロトタイプチェーンの動作方法、prototype プロパティに新しいメソッドを追加する方法を理解する。 |
プロトタイプベースの言語とは?
JavaScript はしばしばプロトタイプベースの言語として記述されます - 継承機能を提供するため、オブジェクトは prototype
オブジェクト を持つことができます。これはテンプレートオブジェクトとして機能し、そこからメソッドやプロパティを継承します。
オブジェクトのプロトタイプオブジェクトもまたメソッドやプロパティを継承するプロトタイプオブジェクトを持つことができます。これはしばしばプロトタイプチェーンと呼ばれ、異なるオブジェクトが他のオブジェクトに定義されたプロパティやメソッドを持つ理由を説明しています。
JavaScript では、あるオブジェクトのインスタンスとそのプロトタイプ (コンストラクタの prototype
プロパティから派生した __proto__
プロパティ) の間にリンクが張られており、そのプロパティとメソッドはプロトタイプの連鎖を辿って発見されます。
メモ: オブジェクトの prototype
(Object.getPrototypeOf(obj)
または非推奨の __proto__
プロパティで取得可能) とコンストラクタ関数のprototype
プロパティの違いを理解することが重要です。
前者は各インスタンス上のプロパティ、後者はコンストラクタ上のプロパティです。つまり、Object.getPrototypeOf(new Foobar())
はFoobar.prototype
と同じオブジェクトを参照しています。
これを少し明確にするための例を見てみましょう。
プロトタイプオブジェクトの理解
ここでは、Person() コンストラクタを書き終えた例に戻ります - ブラウザーで例を読み込んでください。前回の記事で紹介した oojs-class-further-exercises.html の例を使うことができます (ソースコードも参照してください)。
この例では、次のようにコンストラクタ関数を定義しています。
function Person(first, last, age, gender, interests) {
// property and method definitions
this.name = {
'first': first,
'last' : last
};
this.age = age;
this.gender = gender;
//...see link in summary above for full definition
}
次に、このようなオブジェクトインスタンスを作成します。
let person1 = new Person('Bob', 'Smith', 32, 'male', ['music', 'skiing']);
JavaScript コンソールに "person1.
" と入力すると、ブラウザーがこのオブジェクトで利用可能なメンバ名でこれを自動補完しようとするはずです:
このリストでは、person1
のコンストラクタである Person()
で定義されているメンバ - name
、age
、gender
、interests
、bio
、greeting
- が表示されています。しかし、他にも toString
や valueOf
などのメンバがあり、これらのメンバは person1
の prototype オブジェクトの prototype オブジェクト (Object.prototype
) で定義されています。
実際に Object.prototype
で定義されている person1
のメソッドを呼び出すとどうなりますか?例えば
person1.valueOf()
valueOf()
は、呼び出されたオブジェクトの値を返します。この場合、何が起こるかというと
- ブラウザーは最初に、
person1
オブジェクトのコンストラクタPerson()
で定義されているvalueOf()
メソッドが利用可能かどうかをチェックしますが、利用できません - そこで、ブラウザーは
person1
のプロトタイプオブジェクトにvalueOf()
メソッドが利用可能かどうかをチェックします。メソッドがない場合、ブラウザーはperson1
のプロトタイプオブジェクトのプロトタイプオブジェクトをチェックします。メソッドが呼び出されて、すべてがうまくいきました
メモ: プロトタイプチェーンの中では、メソッドやプロパティはあるオブジェクトから別のオブジェクトにコピーされないことを再確認しておきましょう。これらのメソッドやプロパティは、上で説明したようにチェーンを上っていくことでアクセスされます。
メモ: プロトタイプチェーンは、プロパティを取得している間のみ巡回されます。プロパティがオブジェクトに直接設定されたり削除されたり
した場合は、プロトタイプチェーンは走査されません。
メモ: ECMAScript 2015 以前は、オブジェクトの prototype
に直接アクセスする方法は公式にはありませんでした - チェーン内のアイテム間の「リンク」は、JavaScript 言語の仕様で [[prototype]]
と呼ばれる内部プロパティで定義されています (ECMAScript}を参照してください)。
しかし、ほとんどの最新のブラウザーでは、オブジェクトのコンストラクタのプロトタイプオブジェクトを含む __proto__
(アンダースコア 2 個分) というプロパティを提供しています。例えば、person1.__proto__
と person1.__proto__.__proto__
を試してみてください。
ECMAScript 2015 からは、Object.getPrototypeOf(obj)
を介して間接的にオブジェクトのプロトタイプオブジェクトにアクセスすることができます。
prototype プロパティ:継承されたメンバーが定義されている場所
では、継承されたプロパティとメソッドはどこに定義されているのでしょうか? Object
リファレンスページを見ると、左側に多数のプロパティとメソッドが表示されます。上のスクリーンショットでperson1
オブジェクトで使用できた継承されたメンバーの数を超えています。いくつかは継承されており、一部は継承されていません。これはなぜでしょうか?
上で述べたように、継承されたものは prototype
プロパティ (サブネームスペースと呼ぶこともできます) で定義されたものであり、それは Object.prototype.
で始まるものであって、Object.
だけで始まるものではありません。prototype
プロパティの値はオブジェクトであり、基本的には、プロトタイプチェーンのさらに下のオブジェクトに継承させたいプロパティやメソッドを格納するためのバケットです。
そのため、Object.prototype.toString()
、Object.prototype.valueOf()
などは、Person()
コンストラクタから作成された新しいオブジェクトインスタンスを含め、Object.prototype
を継承するあらゆるオブジェクトタイプで利用できます。
Object.is()
、Object.keys()
など、prototype
バケット内で定義されていないメンバは、Object.prototype
を継承するオブジェクトインスタンスやオブジェクトタイプには継承されません。これらは、Object()
コンストラクタ自身でのみ利用可能なメソッド/プロパティです。
メモ: コンストラクタ上で定義されたメソッドが、それ自体が関数であるというのは不思議な感じがします。
まあ、関数はオブジェクトの型でもあります。信じられないかもしれませんが、Function()
のコンストラクタリファレンスを参照してください。
- 既存のプロトタイプのプロパティを自分でチェックすることができます。先ほどの例に戻って、JavaScript コンソールに次のように入力してみてください
Person.prototype
- カスタムコンストラクタのプロトタイプに何も定義していないので、出力はあまり表示されません。デフォルトでは、コンストラクタの
prototype
は常に空から始まります。では、次のようにしてみてくださいObject.prototype
先ほど示したように、Object
の prototype
プロパティに定義された多数のメソッドが、Object
を継承するオブジェクトで利用できるようになっています。
プロトタイプチェーン継承の他の例は、JavaScript の至る所で見ることができます。例えば、String
、Date
、Number
、Array
などのグローバルオブジェクトのプロトタイプに定義されているメソッドやプロパティを探してみてください。これらはすべて、プロトタイプに定義されたいくつかのメンバを持っており、例えばこのように文字列を作るとき
let myString = 'This is my string.';
myString
が最初から、split()
、indexOf()
、replace()
などの便利なメソッドを多数持っている理由です。
メモ: このセクションを理解して、もっと知りたいと思ったら、JavaScript でのプロトタイプの使用 についてのより詳細なガイドを読む価値があります。このセクションは、これらの概念に初めて出会ったときに少しでも理解しやすくするために、意図的に簡略化しています。
警告: prototype
プロパティは JavaScript の中でも最も紛らわしい名前がついている部分の一つです (this
は __proto__
でアクセスできる内部オブジェクトです、覚えていますか?)。代わりに prototype
は、継承したいメンバを定義したオブジェクトを含むプロパティです。
create() の再訪
先ほど、Object.create()
メソッドを使用して新しいオブジェクトのインスタンスを作成する方法を紹介しました。
- 例えば、先ほどの例の JavaScript コンソールでこれを試してみてください
let person2 = Object.create(person1);
create()
が実際に行うことは、指定したプロトタイプオブジェクトから新しいオブジェクトを作成することです。ここでは、person1
をプロトタイプオブジェクトとして使用してperson2
を作成しています。これはコンソールで以下のように入力することで確認できますperson2.__proto__
これで person1
オブジェクトが返されます。
コンストラクタのプロパティ
すべてのコンストラクタ関数は prototype
プロパティを持ち、その値は constructor
プロパティを含むオブジェクトとなります。この constructor
プロパティは、元のコンストラクタ関数を指します。
次のセクションでお分かりのように、Person.prototype
プロパティ (あるいは上のセクションで述べたように、一般的にはコンストラクタ関数の prototype
プロパティに定義されているオブジェクト) は、Person()
コンストラクタを使用して作成されたすべてのインスタンスオブジェクトで利用可能になります。したがって、コンストラクタプロパティは person1
と person2
の両方のオブジェクトでも利用可能です。
-
例えば、コンソールで次のコマンドを試してみてください
これらのインスタンスの元の定義を含む
person1.constructor person2.constructor
Person()
コンストラクタを返します。 巧妙なトリックとしては、constructor
プロパティの最後に括弧を付けて (必要なパラメータを含む)、そのコンストラクタから別のオブジェクトのインスタンスを作成することができます。コンストラクタは結局のところ関数なので、括弧を使用して呼び出すことができます。関数をコンストラクタとして使用したい場合は、new
キーワードを含めて指定する必要があります。 - これをコンソールで試してみてください
let person3 = new person1.constructor('Karen', 'Stephenson', 26, 'female', ['playing drums', 'mountain climbing']);
- では、新しいオブジェクトの機能にアクセスしてみましょう
person3.name.first person3.age person3.bio()
これはよく機能します。頻繁に使用する必要はありませんが、新しいインスタンスを作成したいときに、何らかの理由で元のコンストラクタへの参照が簡単に利用できない場合に非常に便利です。
constructor
プロパティには他の用途もあります。たとえば、オブジェクトのインスタンスがあり、そのインスタンスのコンストラクタの名前を返したい場合は次のようにします。
instanceName.constructor.name
たとえば、これを試してみてください:
person1.constructor.name
メモ: constructor.name
の値は (プロトタイプの継承、バインディング、プリプロセッサ、トランスパイラなどによる) 変わる可能性があります。そのため、より複雑な例では、代わりに instanceof
演算子を使用することになります。
プロトタイプの変更
コンストラクタ関数の prototype
プロパティを変更する例を見てみましょう - メソッドは、コンストラクタから作成されたすべてのオブジェクトインスタンスで利用可能になります。この時点で、最後に Person()
コンストラクタのプロトタイプに何かを追加します。
- oojs-class-further-exercises.html の例に戻り、ソースコードのローカルコピーを作成します。既存の JavaScript の下に、コンストラクタの
prototype
プロパティに新しいメソッドを追加する次のコードを追加しますPerson.prototype.farewell = function() { alert(this.name.first + ' has left the building. Bye for now!'); };
- コードを保存してブラウザーでページを読み込み、テキスト入力に以下のように入力してみてください
person1.farewell();
コンストラクタ内で定義されている人の名前を特徴とする警告メッセージが表示されるはずです。これは本当に便利ですが、さらに便利なのは継承チェーン全体が動的に更新され、コンストラクタから派生したすべてのオブジェクトインスタンスでこの新しいメソッドが自動的に利用できるようになったことです。
ちょっと考えてみましょう。このコードでは、コンストラクタを定義し、そのコンストラクタからインスタンスオブジェクトを作成し、コンストラクタのプロトタイプに新しいメソッドを追加しています。
function Person(first, last, age, gender, interests) {
// プロパティおよびメソッドを定義する
}
let person1 = new Person('Tammi', 'Smith', 32, 'neutral', ['music', 'skiing', 'kickboxing']);
Person.prototype.farewell = function() {
alert(this.name.first + ' has left the building. Bye for now!');
};
しかし、farewell()
メソッドは person1
オブジェクトのインスタンスで利用可能です。そのメンバーは、新たに定義された farewell()
メソッドを含むように自動的に更新されます。
メモ: 逆に、コンストラクタのプロトタイプに定義されたプロパティを delete
演算子を使用して削除すると、他のすべてのクラスインスタンスからもそれぞれのプロパティが削除されます。
上記の例では、delete person1.__proto__.farewell
または delete Person.prototype.farewell
を実行すると、すべての Person
インスタンスから farewell()
メソッドが削除されます。
この問題を軽減するために、代わりに Object.defineProperty()
を使用することができます。
メモ: この例がうまく動作しない場合は、oojs-class-prototype.html の例を見てください (ライブでも参照してください) 。
このように定義されたプロパティは柔軟性に欠けるため、prototype
プロパティで定義されることはほとんどありません。例えば、次のようなプロパティを追加することができます。
Person.prototype.fullName = 'Bob Smith';
これはその person がその名前で呼ばれていないかもしれないので、あまり柔軟性がありません。name.first
と name.last
から fullName
を作成する方がずっと良いでしょう。
Person.prototype.fullName = this.name.first + ' ' + this.name.last;
しかし、これではうまくいきません。この場合、this
は関数スコープではなくグローバルスコープを参照するからです。このプロパティを呼び出すと undefined
を返します。これは、先ほどプロトタイプで定義したメソッドでは問題なく動作したのはそれがオブジェクトのインスタンススコープに正常に転送される関数スコープ内にあるためです。そのため、プロトタイプ上で不変の(つまりだれも変更する必要のない)プロパティを定義することもできますが、一般的にはコンストラクタ内でプロパティを定義する方がうまくいきます。
実際、多くのオブジェクト定義でよく見られるパターンは、コンストラクタ内でプロパティを定義し、プロトタイプ上でメソッドを定義することです。これにより、コンストラクタにはプロパティの定義のみが含まれ、メソッドは別のブロックに分割されるため、コードが読みやすくなります。例えば、以下のようになります。
// Constructor with property definitions
function Test(a, b, c, d) {
// プロパティ定義
}
// 最初のメソッド定義
Test.prototype.x = function() { ... };
// 第二のメソッド定義
Test.prototype.y = function() { ... };
// など
このパターンは、Piotr Zalewa 氏の学校計画のアプリの例で実際に見られます。
あなたのスキルをテストしてみましょう!
この記事はここまでですが、最も重要な情報を覚えていますか?先に進む前に、この情報を保持しているかどうかを確認するために、さらにいくつかのテストを見つけることができます。あなたのスキルをテストする: オブジェクト指向 JavaScript (en-US) を参照してください。
この一連のテストは次の記事で紹介する知識に依存していることに注意してください。なので、試してみる前に、まずそれを読んでみるといいかもしれません。