Shoeisha Technology Media

CodeZine(コードジン)

記事種別から探す

プロトタイプ(prototype)によるJavaScriptのオブジェクト指向

JavaScriptによるオブジェクト指向プログラミング 第3回

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2005/12/08 12:00

このシリーズは、JavaScript(ECMAScript)における概念と言語構造の理解を助けるためのドキュメントです。第3回目の本稿では、JavaScript(ECMAScript)におけるオブジェクトの最重要機構であるプロトタイプについて解説します。

はじめに

 JavaScriptはオブジェクト指向言語です。しかし、そのオブジェクトの性質は、他に良く知られているオブジェクト指向言語のJavaやC++と大きく異なっています。

 そこでこの記事では、なぜそのような違いがあるのか、JavaScriptにおけるオブジェクト指向の言語思想を見ていくと共に、その違いの根幹であるプロトタイプについて解説していきたいと思います。

 なお、この記事はJavaScriptの解説ですが、その内容は、標準仕様のECMAScriptで扱われる範囲に基づいています。従って、同じくECMAScriptを元にしている言語(JScriptActionScript)でも通じる内容になっています。

対象読者

 プログラミングの基本的な知識、ならびにオブジェクトやメソッドと言った基礎的な概念については、ここでは解説しません。最低限、オブジェクト指向プログラミングについて理解をしている人を対象としています。

 この記事は、主に以下のような人を対象としています。

  • サーバサイドJavaを行っていて、JSPなどでJavaScriptの知識が必要になった人
  • JavaScriptをより深く使いこなしたい人
  • オブジェクト指向プログラミングを別のアプローチから捉えなおしてみたい人
  • JavaScriptでクラスを作る方法は知っているが、何故そのように書くとクラスを作れるのかわからない人

必要な環境

 ECMAScript 3rd editionの実装系。例えば、下記のような環境。

  • Microsoft Internet Explorer 5.5以上
  • Firefox 1.0以上

目次

  1. クラスベースとプロトタイプベース
  2. コンストラクタという機構
  3. 暗黙の参照
  4. prototypeオブジェクト
  5. プロトタイプチェーン
  6. hasOwnProperty

クラスベースとプロトタイプベース

 まず実装の話に入る前に、オブジェクト指向について確認したいと思います。オブジェクト指向とは果たして何なのでしょうか?

 端的に表すと「ある事象をオブジェクトという単位で分解・整理して、認識・再現すること」です。つまりプログラミングならば、「最終的に作りたいシステム」が事象となり、このシステムをオブジェクトという単位に分解して、コンピュータ上で再現することがオブジェクト指向プログラミングになる訳です。

 そして、JavaScriptやJava、C++などのオブジェクト指向言語というのは、事象を分解したオブジェクト群をプログラムとして再現しやすいように設計された言語という事になります。

 JavaやC++といった良く知られるオブジェクト指向言語は、このプログラムとして再現する際にオブジェクトを定義する方法として、クラスという概念を用います。というのは、全てのオブジェクトにはクラスという雛形が必ず存在し、その雛型を実体化することでオブジェクトは生成される、という考え方です。したがって、JavaやC++のオブジェクトは必ずクラスが存在し、メンバもクラスの定義から逸脱する事ができません。また、オブジェクトはクラスから実体化されるのでインスタンスと呼ばれます。

 このようなクラス-インスタンスという概念を使用するオブジェクト指向言語のことを、クラスベースオブジェクト指向言語(Class Based Object Oriented Language)と言います。

 それとは異なり、JavaScriptではクラス-インスタンスという考え方をしません(*1)。オブジェクトは別なオブジェクトを元(プロトタイプ)にして独自の特徴を付加することで存在する、という考え方をします。このようなオブジェクト指向言語のことを、プロトタイプベースオブジェクト指向言語(Prototype Based Object Oriented Language)と言います。

*1
 あくまで「基本的には」ですが。JavaScriptにもクラス-インスタンスという概念は存在しています。

 このようにオブジェクトに対する考え方がまるで違うので、JavaScriptとJavaではオブジェクトの性質が異なっているのです。JavaScriptでは、オブジェクト自身が独自の特徴を付加するので、第1回で見たように、オブジェクトにメンバを追加したり削除したりすることができる、という訳です。

コンストラクタという機構

 では、JavaScriptでオブジェクトを生成する方法を見ていきましょう。JavaScriptでオブジェクトを生成するには、コンストラクタとnewという演算子を使用します。

 そのコンストラクタとは一体何なのでしょうか。実はコンストラクタとは関数オブジェクトの事です。全ての関数はオブジェクトを生成するコンストラクタになる可能性があります。まずは一番シンプルな例を見てみましょう(List3.1)。

List3.1 単純なコンストラクタ
function SimpleConstructor() {}

var obj = new SimpleConstructor();

 普通にfunction文で定義した関数にnewをつけて呼び出しているだけです。こうすることで、新しいオブジェクトが生成され、戻り値として返されます。return文を記述しなくても生成されたオブジェクトが返される事に注意してください。

 さて、それではもう少しコンストラクタの機構を詳しく見ていきましょう。通常、コンストラクタとして呼ばれる関数の中では、生成しようとするオブジェクトの初期化を行います。共通のプロパティなどをコンストラクタ内で定義するのです(List3.2)。

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)。

*2
 もちろんdelete演算子を使用すればプロパティを削除できるので、同じコンストラクタから作成されても常に必ず同じプロパティを持つとは限らない事に注意してください。

暗黙の参照

 さて、JavaScriptにおけるオブジェクトの生成について見たところで、今回の本題であるプロトタイプについて見ていきましょう。

 プロトタイプベースのオブジェクト指向言語では、オブジェクトは別のオブジェクトをプロトタイプとしてできていると考えます。JavaScriptではこれを暗黙の参照という形で実現しています。オブジェクトAをプロトタイプとしているオブジェクトBは、オブジェクトAに対し暗黙の参照を持っているという状態になります。

 Javaで言うなれば、非staticなinner classのインスタンスがenclosing classのインスタンスに対し、暗黙の参照を持っている状態を考えてもらえば良いでしょうか。

 つまりList3.3のような状態になります。

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)。

List3.4 prototypeオブジェクト
function PrototypeTestConstructor() {}

// prototype オブジェクトに prop1 というプロパティを設定。
PrototypeTestConstructor.prototype.prop1 = 30;

var obj = new PrototypeTestConstructor();
alert(obj.prop1);    // 30 と表示される。

 つまりPrototypeTestConstructorによって生成されたオブジェクトは、PrototypeTestConstructor.prototypeが指すオブジェクトを暗黙的に参照するようになります。

 さて、ここでふと疑問に思うことがあります。あるコンストラクタをnewしてできるオブジェクトが全てそのprototypeへの暗黙の参照を持つのであれば、そのプロパティはクラス変数の様にnewされたオブジェクトで共有されてしまうのではないでしょうか?

 確かに、共有されてしまいます。しかしそれが実際に問題になることはありません。その理由を以下に示します。まずは次の例を見てください(List3.5)。

List3.5 代入はprototypeをたどらない
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 αの状態を表しています。

Figure3.1 List3.5 αの状態
Figure3.1 List3.5 αの状態

 この時objA.prop1を呼び出すと、objA自身にはprop1が無いため、暗黙の参照をたどり、prototypeにてprop1を見つけ30と評価されます。

 しかしその後、代入によってFigure3.2の状態になります。

Figure3.2 List3.5 βの状態
Figure3.2 List3.5 βの状態

 この場合、objA.prop1を呼び出すと、objA自身にはprop1があるため、その値である100と評価され、objB.prop1を呼び出すと、暗黙の参照をたどり30と評価されるのです。

 この仕組みがプロトタイプベース言語であるJavaScriptのオブジェクトの根幹となります。

プロトタイプチェーン

 さて、前節では話を単純にするために1階層での例でしたが、コンストラクタのprototypeには、オブジェクトも代入できますので、prototypeに代入したオブジェクトが別のオブジェクトをプロトタイプにしている事もあります。そもそも全てのオブジェクトはObject.prototypeを暗黙的に参照しています。

 こうしたプロトタイプの連鎖のことをプロトタイプチェーンと呼びます。

List3.6 プロトタイプチェーン
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)。

List3.7 hasOwnProperty
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に寄稿いたしました。



  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

  • 中村 学(ナカムラ マナブ)

    イー・デスク株式会社 開発部 部長。業務ではJava/PHPによるWebアプリケーション開発を行っている。学生時代はユーザインターフェイスデザインを専攻し、アクセシビリティ、ユーザビリティの観点からスタイルシート、JavaScript等の技術に造詣を深める。最近はRubyとLispに注目している。...

バックナンバー

連載:JavaScriptによるオブジェクト指向プログラミング
All contents copyright © 2005-2017 Shoeisha Co., Ltd. All rights reserved. ver.1.5