JavaScriptのprototypeを理解する
JavaScriptにはprototypeという仕組みがあります。
プログラミング入門者の方はもちろんのこと、他言語を学習済みの方でもあまり馴染みがないかもしれません。
しかしJavaScriptの基本的な仕組みに関係しているprototypeの理解は、JavaScriptを使いこなすにあたって避けて通れないものです。
今回はこのprototypeについて実際のコードと共に解説していきます。
JavaScriptのprototypeとは
JavaScriptにはprototypeという仕組みがあります。
PHPやRuby、Pythonなど他言語には登場しない言葉なので、聞き馴染みのない方が多いかもしれません。
prototypeはJavaScriptをプログラミング言語として構成する、もっとも基本的な仕組みのひとつです。
他言語に登場する概念のうち、もっともprototypeに近しい概念は「継承」です。
継承とは、あるオブジェクトが他のオブジェクトの性質(メソッドやプロパティ等)を引き継ぐ仕組みのことで、prototypeの仕組みはこれに似ています。
JavaScript初学者の方は、ひとまず**「prototypeとは継承のようなものだ」**という認識で理解をしておくと楽かもしれません。
prototypeの性質
prototypeは他言語における「継承」に似た性質を持ちます。具体例について紹介します。
- prototypeは、オブジェクトの内部プロパティのひとつである。
- 内部プロパティ[prototype]には、何らかのオブジェクト(またはnull)が設定される。
- 内部プロパティ[prototype]は、あるオブジェクトが自身で保有していないプロパティを命令されたとき、自身の代わりにそのプロパティを、内部プロパティ[prototype]に設定されたオブジェクトから探索しに行く参照先である。
- 命令されたプロパティを自身の代わりに内部プロパティ[prototype]が保有している場合、オブジェクトはそれを返却する。
以上の性質により、結果的にprototypeはあるオブジェクトが他のオブジェクトの性質を引き継ぐ仕組みであるかのように振る舞います。正確には引き継いだわけではなく、内部的には自身の代わりに他のオブジェクトの性質を参照し、自身のものであるかのように振る舞うという仕組みであることを注意しておくとよいでしょう。
prototypeで実現できること
prototypeを活用するメリットのひとつは、コードの簡略化です。
同じメソッドを複数のオブジェクトのプロパティとして何個も実装する必要があるとき、prototypeを活用しない場合はひたすらに同じメソッドを定義しつづけなければなりません。しかし、prototypeを活用するとこれが簡略化できます。
もうひとつの恩恵は、プログラム実行時の省メモリ化です。
同じメソッドを何個も実装するとプログラムはより多くのメモリを必要とします。一方、prototypeを利用することで、同じメソッドは一度定義すればあとはそれを参照して使いまわすことができます。
そのため、プログラムが使用するメモリを少なくすることができるのです。
なおここでは説明のためメソッドを例に出しましたが、JavaScriptにおいてメソッドとはプロパティのひとつです。また、メソッド(function)もJavaScriptにおいてはオブジェクトとして扱われています。
JavaScriptにおいてプロパティにはオブジェクトであれば何でも設定することができるため、メソッドではなく固定値や配列、連想配列などを扱う場合にも上述のようなprototypeの活用が可能です。
JavaScriptのprototypeを使いこなす
前項の説明でprototypeについてprototypeは、オブジェクトの内部プロパティのひとつであると表現しました。
内部プロパティとは、JavaScriptが内部的に持つプロパティで、通常のプロパティと異なり直接参照・編集することのできないプロパティです。しかし、JavaScriptには内部プロパティ[prototype]を扱うための様々な仕組みが備わっています。
オブジェクトのprototypeを確認する
まず内部プロパティ[prototype]に何が設定されているかを確認する方法を解説します。
JavaScriptにおいて内部プロパティ[prototype]は秘匿されているため、通常のプロパティのように直接参照することはできません。
その代わり、JavaScriptには内部プロパティを確認するためのメソッドとして「Object.getPrototypeOf()
メソッド」が用意されています。
Object.getPrototypeOf()
メソッドは次のように使用します。
1.内部プロパティ[prototype]を確認したいオブジェクトを引数に指定する
2.メソッドを実行することで、内部プロパティ[prototype]に設定されたオブジェクトを返却する
以下がObject.getPrototypeOf()
メソッドの使用例です。
配列や空オブジェクトには最初から内部プロパティ[prototype]が設定されているため、Object.getPrototypeOf()
メソッドを利用してそれらの初期設定されたprototypeを確認しています。
// Arrayのprototype: Function型のオブジェクト
// 出力:ƒ () { [native code] }
console.log(Object.getPrototypeOf(Array))
// Function型のオブジェクトのprototype:Object型のオブジェクト
// 出力:{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Array)))
// Object型のオブジェクトのprototype:null
// 出力:null
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(Array))))
オブジェクトのprototypeを書き換える
次に内部プロパティ[prototype]に設定されたオブジェクトを書き換える方法を解説します。
内部プロパティ[prototype]を確認するために使用したObject.getPrototypeOf()
と同様に、内部プロパティ[prototype]を書き換えるためのメソッドもまた、Objectオブジェクトが保有しています。
内部プロパティ[prototype]は、【Object.setPrototypeOf()メソッド】で書き換えることが可能です。Object.setPrototypeOf()
メソッドの引数には、以下を設定します。
第一引数:内部プロパティ[prototype]に設定するオブジェクト
第二引数:内部プロパティ[prototype]を書き換える対象のオブジェクト
このように引数を指定し実行することで、第一引数に指定したオブジェクトが、第二引数のオブジェクトの内部プロパティ[prototype]に設定されます。
実際の使用例は以下の通りです。
以下では2つのオブジェクト(obj1, obj2)を定義し、このうちobj1の内部プロパティ[prototype]にobj2を設定しています。
// オブジェクト作成
var obj1 = {type: "human"}
var obj2 = {name: "sato"}
// オブジェクト作成時点のprototype:Object型のオブジェクト
// 出力:{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
console.log(Object.getPrototypeOf(obj1))
console.log(Object.getPrototypeOf(obj2)) // obj1と同様の出力
// prototype書き換え(obj2のprototypeをobj1に設定)
Object.setPrototypeOf(obj2, obj1)
// prototype書き換え後のobj2のprototype:obj1
// 出力: {type: "human"}
console.log(Object.getPrototypeOf(obj2))
// obj2自身のプロパティ(name)
// 出力: sato
console.log(obj2.name)
// obj2のprototypeのプロパティ(type)
// 出力: human
console.log(obj2.type)
prototypeで継承を実現する
最後に、prototypeを利用して他言語で言うところの「継承」を実現する方法を解説します。
継承は多くのオブジェクト指向言語において、あるクラスのオブジェクトが作成されるとき、親として指定したクラスのメソッド等を引き継ぐという形を取ります。なお、クラスとはJavaScriptにおけるオブジェクトの型枠のようなものです。
作成されてから引き継ぐ対象を指定するのではなく、作成される前に「自身が引き継ぐオブジェクトはこれだ」と定義していることが継承のポイントです。
こうすることで、新しくオブジェクトを作成する度に継承するオブジェクトを指定する手間が省かれ、コードをより簡略化できます。
JavaScriptにおいて、オブジェクトを作成する前に継承相手を決めておくにはどのようにすればよいのでしょうか。
先述のsetPrototypeOf()
では、オブジェクトを作成した後に継承相手(内部プロパティ[prototype])を設定することしかできません。
このようなときのために、JavaScriptには内部プロパティ[prototype]とは別に、prototypeプロパティが用意されています。名前が内部プロパティ[prototype]と極めて酷似しているため、混同しないよう気を付けてください。
(この解説のために、本記事では内部プロパティ[prototype]をprototypeと略さずに解説しました。)
prototypeプロパティとは、Function型のオブジェクトに用意されたプロパティのひとつです。
Function型のオブジェクトとはつまり、JavaScriptにおけるメソッド(function)です。
Function.prototypeプロパティは次のような性質を持ちます。
- Function.prototypeプロパティは内部プロパティ[prototype]とは異なり、直接参照・書き換えの可能な通常のプロパティである。
- Function.prototypeプロパティに定義したオブジェクトは、そのFunction型のオブジェクトが【new構文によりオブジェクトを新規作成するとき】に、その新たなオブジェクトの内部プロパティ[prototype]として設定される。
つまりFunction.prototypeプロパティは、他言語の継承と同様に、新しく作成するオブジェクトの親(内部プロパティ[prototype])を事前に定義することができる仕組みであると言えます。Function.prototypeプロパティを活用した実際の継承方法は以下の通りです。
以下のコードは次のような処理順になっています。
1.animalコンストラクタとhumanコンストラクタを定義する。
2.humanコンストラクタ(コンストラクタはFunction型のオブジェクト)のprototypeプロパティに type = "human" のanimalオブジェクトを設定する。
3.name = "sato" のhumanオブジェクトを新規作成する。
4.humanオブジェクト自身は保有していないtypeプロパティを、継承した親(animalオブジェクト)から取得できるかを確認する。
var animal = function(type) { this.type = type }
var human = function(name) { this.name = name }
// コンストラクタはFunction型のオブジェクト
// 出力:ƒ () { [native code] }
console.log(Object.getPrototypeOf(animal))
console.log(Object.getPrototypeOf(human))
// Function型のオブジェクトには『prototype』とは別に『prototypeプロパティ』が用意されている
// 出力:{constructor: ƒ}
console.log(animal.prototype)
console.log(human.prototype)
// humanの『prototypeプロパティ』を『animalコンストラクタで作成したオブジェクト({type: "human"})』に書き換える
// 出力:animal {type: "human"}
human.prototype = new animal("human")
console.log(human.prototype)
// humanコンストラクタでオブジェクトを作成する
var obj = new human("sato")
// オブジェクト自身のプロパティ
// 出力:sato
console.log(obj.name)
// オブジェクトのprototype
// 出力:animal {type: "human"}
console.log(Object.getPrototypeOf(obj))
// オブジェクトのprototypeのプロパティ
// 出力:human
console.log(obj.type)
このようにFunction.prototypeプロパティを活用することで、new構文で新規作成するオブジェクトの親(内部プロパティ[prototype])を事前に指定することができます。
内部プロパティ[prototype]とFunction.prototypeプロパティの違いに留意しつつ、継承を利用した処理の実装を活用していきましょう。