prototype.jsを読み解く

第1回 Prototypeライブラリ(1~197行目)

この記事を読むのに必要な時間:およそ 12 分

Classオブジェクト

0034: var Class = {
0035:   create: function() {
0036:     return function() {
0037:       this.initialize.apply(this, arguments);
0038:     }
0039:   }
0040: }
0041: 

Prototypeライブラリにおいて(賛否両論はありますが)特徴的な所がこのClassオブジェクトでしょう。JavaScriptのオブジェクトとしては,単にcreateというプロパティをもったオブジェクトです。このcreateには,37行目にあるように,⁠this.initialize.apply() を呼び出す関数オブジェクトを返す関数」が代入されています。

一般的なクラスベースのオブジェクト指向型言語とは異なり,JavaScriptには言語としてのクラスという概念がありません。

JavaScriptのprototypeプロパティを用いたオブジェクトの継承は,使い方次第で柔軟に応用できるのですが,クラスという概念に慣れている開発者にとっては,取っ付きにくい部分があるのも確かでしょう。

そこで,このClassオブジェクトを使うことにより,クラスベースの言語に近い表現ができるようになります。

まず,Prototypeライブラリを使わずに型を持ったオブジェクトを生成する方法を見てみましょう。

var Foo = function(arg1, arg2) {
  this.prop1 = "p1";
};
Foo.prototype.prop2 = "default value";
var instance1 = new Foo("bar", "baz");
alert(instance1.prop1);	 // "p1"が表示される
alert(instance1.prop2);	 // "default value"が表示される

このように,まずは関数オブジェクトが必要となります。これがコンストラクタの役割をはたします。インスタンスが生成される際に,デフォルトでプロパティを割り当てたい場合には,プロトタイプチェーンを使った継承を行います。それには,その関数オブジェクトのprototypeプロパティ以下にプロパティを設定します。

準備ができたら,その関数オブジェクトに対してnew演算子を適用します。返り値が新しいオブジェクトインスタンスとなり,特定の雛型(デフォルト値)を持った状態で生成されます。

そして,インスタンスがメモリ上に生成された後で,その関数オブジェクト自体がthisという変数が定義された状態で呼び出されることになります。

次に,Prototype ライブラリではどう書くかというと,以下のようになります。

var NewClass = Class.create();
NewClass.prototype = {
  initialize: function(arg1, arg2) {
    this.attr1 = 'value1';
    this.attr2 = 'value2';
  },
  memberFunction: function(arg) {
  },
  prop1: null
}
var instance2 = new NewClass(1, 2);

Class.create()を使ってクラスの雛形を作り,そのprototypeプロパティにメソッドやプロパティを設定します。

C++やJavaなどの言語では,クラスを定義して,その中にコンストラクタやメソッド,メンバ変数などが定義されますが,それと同じように,インスタンスの雛形としての定義をこのprototypeプロパティへの代入で実現しています。

ここで,initializeというプロパティだけが特別扱いされ,このプロパティに関数オブジェクトが設定されていると,インスタンスを生成する際に自動的にコンストラクタとして呼び出されます。

さて,Prototypeライブラリでの Class.create() の実装に戻ります。まず,Class.create()を呼び出すことで,35行目で定義されている関数オブジェクトが呼び出されます。この関数だけを抜き出すと,

function() {
  return function() {
    this.initialize.apply(this, arguments);
  }
}

となっています。そのため,Class.create()を実行すると,

function() {
  this.initialize.apply(this, arguments);
}

という関数オブジェクトを返すということになります。

先ほど,Prototypeライブラリを使わない方法では,まず関数オブジェクトを定義していました。この関数の中身は関数オブジェクトを返すものなので,例においてvar NewClassという変数にはその関数オブジェクトが代入されます。

その次の行から,NewClass.prototypeにオブジェクトを代入していますが,ここではNewClass.prototype.initialize()というプロパティに関数オブジェクトを定義しています。これにより,NewClassがインスタンス化される際に,NewClassに代入してある関数が実行され,その中でthis.intialize.apply()が呼び出されます。

apply()は,JavaScriptのFunctionオブジェクトに予め定義されているメソッドで,thisとなるべきオブジェクトと,引数配列を引数にとり,そのFunctionオブジェクトを実行します。

その結果,initialize()メソッドが実行される際には,インスタンス自身がthisとして設定された状態になります。

Abstractオブジェクト

0042: var Abstract = new Object();
0043: 

Abstractオブジェクトは,この段階では単なるプレースホルダになっていて,後ほどAbstract.Insertionなどが配置されます。

Object.extend()

0044: Object.extend = function(destination, source) {
0045:   for (var property in source) {
0046:     destination[property] = source[property];
0047:   }
0048:   return destination;
0049: }
0050: 

ECMA-262に準拠している環境においては,Objectオブジェクトは予め定義されています。ここではまず,Objectオブジェクトにいくつものプロパティを追加する前に,既存のオブジェクトにプロパティを追加するのに便利なObject.extend()という関数を定義しています。

45行目にあるfor文により,source内のプロパティ(のうちDontEnum属性が付いていないもの)を列挙します。その各プロパティを,destination側のオブジェクトにひとつひとつ代入してコピーし,その結果のdestinationを返す,という関数です。

これにより,既にいくつかプロパティが定義されているオブジェクトに対して,上書きするのではなく追加する形でプロパティを増やすことができます。以下のような使い方になります。

Object.extend(既存オブジェクト, {
    プロパティ:値
})

この関数は,Prototypeライブラリ全体で使われており,使用頻度の高い物となっています。

著者プロフィール

栗山淳(くりやまじゅん)

S2ファクトリー株式会社株式会社イメージソース所属。
本業はWeb制作会社の裏方。得意分野はFreeBSDやPerlのはずだが,必要に迫られるとHTML/CSSやJavaScriptも書く。