はじめに
JavaScriptはオブジェクト指向言語です。しかし、そのオブジェクトの性質は、他に良く知られているオブジェクト指向言語のJavaやC++と大きく異なっています。
そこでこの記事では、なぜそのような違いがあるのか、JavaScriptにおけるオブジェクト指向の言語思想を見ていくと共に、その違いの根幹であるプロトタイプについて解説していきたいと思います。
なお、この記事はJavaScriptの解説ですが、その内容は、標準仕様のECMAScriptで扱われる範囲に基づいています。従って、同じくECMAScriptを元にしている言語(JScript、ActionScript)でも通じる内容になっています。
対象読者
プログラミングの基本的な知識、ならびにオブジェクトやメソッドと言った基礎的な概念については、ここでは解説しません。最低限、オブジェクト指向プログラミングについて理解をしている人を対象としています。
この記事は、主に以下のような人を対象としています。
- サーバサイドJavaを行っていて、JSPなどでJavaScriptの知識が必要になった人
- JavaScriptをより深く使いこなしたい人
- オブジェクト指向プログラミングを別のアプローチから捉えなおしてみたい人
- JavaScriptでクラスを作る方法は知っているが、何故そのように書くとクラスを作れるのかわからない人
必要な環境
ECMAScript 3rd editionの実装系。例えば、下記のような環境。
- Microsoft Internet Explorer 5.5以上
- Firefox 1.0以上
目次
クラスベースとプロトタイプベース
まず実装の話に入る前に、オブジェクト指向について確認したいと思います。オブジェクト指向とは果たして何なのでしょうか?
端的に表すと「ある事象をオブジェクトという単位で分解・整理して、認識・再現すること」です。つまりプログラミングならば、「最終的に作りたいシステム」が事象となり、このシステムをオブジェクトという単位に分解して、コンピュータ上で再現することがオブジェクト指向プログラミングになる訳です。
そして、JavaScriptやJava、C++などのオブジェクト指向言語というのは、事象を分解したオブジェクト群をプログラムとして再現しやすいように設計された言語という事になります。
JavaやC++といった良く知られるオブジェクト指向言語は、このプログラムとして再現する際にオブジェクトを定義する方法として、クラスという概念を用います。というのは、全てのオブジェクトにはクラスという雛形が必ず存在し、その雛型を実体化することでオブジェクトは生成される、という考え方です。したがって、JavaやC++のオブジェクトは必ずクラスが存在し、メンバもクラスの定義から逸脱する事ができません。また、オブジェクトはクラスから実体化されるのでインスタンスと呼ばれます。
このようなクラス-インスタンスという概念を使用するオブジェクト指向言語のことを、クラスベースオブジェクト指向言語(Class Based Object Oriented Language)と言います。
それとは異なり、JavaScriptではクラス-インスタンスという考え方をしません(*1)。オブジェクトは別なオブジェクトを元(プロトタイプ)にして独自の特徴を付加することで存在する、という考え方をします。このようなオブジェクト指向言語のことを、プロトタイプベースオブジェクト指向言語(Prototype Based Object Oriented Language)と言います。
このようにオブジェクトに対する考え方がまるで違うので、JavaScriptとJavaではオブジェクトの性質が異なっているのです。JavaScriptでは、オブジェクト自身が独自の特徴を付加するので、第1回で見たように、オブジェクトにメンバを追加したり削除したりすることができる、という訳です。
コンストラクタという機構
では、JavaScriptでオブジェクトを生成する方法を見ていきましょう。JavaScriptでオブジェクトを生成するには、コンストラクタとnew
という演算子を使用します。
そのコンストラクタとは一体何なのでしょうか。実はコンストラクタとは関数オブジェクトの事です。全ての関数はオブジェクトを生成するコンストラクタになる可能性があります。まずは一番シンプルな例を見てみましょう(List3.1)。
function SimpleConstructor() {} var obj = new SimpleConstructor();
普通にfunction
文で定義した関数にnew
をつけて呼び出しているだけです。こうすることで、新しいオブジェクトが生成され、戻り値として返されます。return
文を記述しなくても生成されたオブジェクトが返される事に注意してください。
さて、それではもう少しコンストラクタの機構を詳しく見ていきましょう。通常、コンストラクタとして呼ばれる関数の中では、生成しようとするオブジェクトの初期化を行います。共通のプロパティなどをコンストラクタ内で定義するのです(List3.2)。
function PropSetConstructor() { this.prop1 = 10; this.prop2 = 20; } var obj1 = new PropSetConstructor(); alert(obj1.prop1); // 10 と表示される。 var obj2 = new PropSetConstructor(); alert(obj2.prop1); // 10 と表示される。
コンストラクタとして関数オブジェクトを実行した場合、this
には今新たに生成されようとしているオブジェクトが入ります。従って、this
にプロパティを設定してやれば、new
して作成されるオブジェクトの初期定義を行う事ができます。こうする事で、同じコンストラクタを用いて複数のオブジェクトを生成すると、同じプロパティを持つオブジェクトを簡単に作成する事ができます(*2)。
delete
演算子を使用すればプロパティを削除できるので、同じコンストラクタから作成されても常に必ず同じプロパティを持つとは限らない事に注意してください。暗黙の参照
さて、JavaScriptにおけるオブジェクトの生成について見たところで、今回の本題であるプロトタイプについて見ていきましょう。
プロトタイプベースのオブジェクト指向言語では、オブジェクトは別のオブジェクトをプロトタイプとしてできていると考えます。JavaScriptではこれを暗黙の参照という形で実現しています。オブジェクトAをプロトタイプとしているオブジェクトBは、オブジェクトAに対し暗黙の参照を持っているという状態になります。
Javaで言うなれば、非static
なinner classのインスタンスがenclosing classのインスタンスに対し、暗黙の参照を持っている状態を考えてもらえば良いでしょうか。
つまりList3.3のような状態になります。
var objectA = ... // objectB は objectA に対し暗黙の参照を持っている // (objectAはobjectBのプロトタイプである)。 var objectB = ... objectA.hoge = 10; // objectA の hoge プロパティをここで設定する。 alert(objectB.hoge); // 10 と評価される。
すなわち、あるオブジェクトがプロパティを評価された時、自分自身がそのプロパティを持っていなければ、暗黙の参照をたどって、その先のオブジェクトのプロパティを評価します。
これがJavaScriptにおけるプロトタイプの仕組みです。
prototypeオブジェクト
それでは、具体的にプロトタイプを指定する機構を見てみましょう。ここで再びコンストラクタが出てきます。
実は、全ての関数オブジェクトはprototype
というプロパティを保持しています。関数オブジェクトを定義した直後では、このprototype
には何もプロパティを持たないシンプルなオブジェクトを参照していますが、別のオブジェクトを代入したり、新たなプロパティを設定したりする事が可能です。
そして、その関数オブジェクトをコンストラクタとして生成されたオブジェクトは、コンストラクタのprototype
プロパティに代入されているオブジェクトに対し、暗黙の参照を持つのです。つまりprototype
が指すオブジェクトがプロトタイプとなるというわけです。
具体的にコードを見てみましょう(List3.4)。
function PrototypeTestConstructor() {} // prototype オブジェクトに prop1 というプロパティを設定。 PrototypeTestConstructor.prototype.prop1 = 30; var obj = new PrototypeTestConstructor(); alert(obj.prop1); // 30 と表示される。
つまりPrototypeTestConstructor
によって生成されたオブジェクトは、PrototypeTestConstructor.prototype
が指すオブジェクトを暗黙的に参照するようになります。
さて、ここでふと疑問に思うことがあります。あるコンストラクタをnew
してできるオブジェクトが全てそのprototype
への暗黙の参照を持つのであれば、そのプロパティはクラス変数の様にnew
されたオブジェクトで共有されてしまうのではないでしょうか?
確かに、共有されてしまいます。しかしそれが実際に問題になることはありません。その理由を以下に示します。まずは次の例を見てください(List3.5)。
function Constructor() {} Constructor.prototype.prop1 = 30; var objA = new Constructor(); var objB = new Constructor(); // [α] alert(objA.prop1) // 30 と表示される。 alert(objB.prop1) // 30 と表示される。 objA.prop1 = 100; // [β] alert(objA.prop1) // 100 と表示される。 alert(objB.prop1) // ???
List3.5の最後のalert
は何と表示されるでしょうか? 実はこれは30と表示されます。
というのも、読み取り評価の時は、暗黙の参照をたどるのですが、代入やdelete
演算子は、たどらないのです。従って、objA.prop1 = 100;
を行った時点で、objA
そのものにprop1
というプロパティが新たに作られ、そこに100が代入されるのです。よってConstructor.prototype.prop1
の値は変わらないままとなります。
ちょっと分かりづらいので図にしてみましょう。Figure3.1はList3.5 αの状態を表しています。
この時objA.prop1
を呼び出すと、objA
自身にはprop1
が無いため、暗黙の参照をたどり、prototype
にてprop1
を見つけ30と評価されます。
しかしその後、代入によってFigure3.2の状態になります。
この場合、objA.prop1
を呼び出すと、objA
自身にはprop1
があるため、その値である100と評価され、objB.prop1
を呼び出すと、暗黙の参照をたどり30と評価されるのです。
この仕組みがプロトタイプベース言語であるJavaScriptのオブジェクトの根幹となります。
プロトタイプチェーン
さて、前節では話を単純にするために1階層での例でしたが、コンストラクタのprototype
には、オブジェクトも代入できますので、prototype
に代入したオブジェクトが別のオブジェクトをプロトタイプにしている事もあります。そもそも全てのオブジェクトはObject.prototype
を暗黙的に参照しています。
こうしたプロトタイプの連鎖のことをプロトタイプチェーンと呼びます。
var objA = new Object(); objA.prop1 = 10; function Func1() {} Func1.prototype = objA; var objB = new Func1(); function Func2() {} Func2.prototype = objB; var objC = new Func2(); alert(objC.prop1); // 10 と評価される。
hasOwnPropertyメソッド
以上までがJavaScriptのプロトタイプのメカニズムです。JavaやC++といったクラスベースの言語には根本的に無い考え方なので若干戸惑うかも知れませんが、全てのオブジェクトがが実体を持った別のオブジェクトに連鎖しているということが分かれば整理できると思います。
さて、ではあるオブジェクトのプロパティを評価した際、仮に何らかの値が返ってきたとしても、そのオブジェクト自身がプロパティを持っているのか、そのオブジェクトのプロトタイプが持っているのか、判断することができません。
そこで、JavaScriptでは全てのオブジェクトに(すなわちObject.prototype
に)、hasOwnProperty
というメソッドが定義されています。これは引数で与えた文字列に一致するプロパティを、そのオブジェクト自身が持っているかどうかを判断してくれるメソッドです。
具体例を見てみましょう(List3.7)。
function Constructor() {} Constructor.prototype.prop1 = 30; var objA = new Constructor(); // objA 自体は持っていないので false。 alert(objA.hasOwnProperty("prop1")); objA.prop1 = 100; // objA 自体が持っているので true。 alert(objA.hasOwnProperty("prop1"));
このhasOwnProperty
メソッドを使うことで、そのオブジェクト自身にプロパティがあるかどうかを判断することができます。
まとめ
今回は、以下のようなプロトタイプのメカニズムについて学びました。
- JavaScriptは、既存のオブジェクトを基に新たなオブジェクトを生成する、プロトタイプベースのオブジェクト指向言語である。
- 関数オブジェクトを
new
演算子で使用することによって、新たなオブジェクトを生成するコンストラクタになる。 - 関数オブジェクトの
prototype
プロパティに代入されているオブジェクトは、その関数オブジェクトをコンストラクタとして生成されたオブジェクトから暗黙の参照をされている。 - この暗黙の参照の連鎖をプロトタイプチェーンと呼ぶ。
hasOwnProperty
メソッドによって、オブジェクト自身にプロパティがあるかどうか判断することができる。
参考資料
本稿は、Starry Night 『JavaScript講座』 にて公開していた記事の続きにあたります。連載を再開するにあたり、再編集を行いCodezineに寄稿いたしました。
- Ecma International 『Standard ECMA-262』
- 『Under Translation of ECMA-262 3rd Edition』