prototype.jsを読み解く

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

はじめに

今回から、Prototypeライブラリについて、中身のコードを読みながら、実装として中で何が行われているのかを見ていこうと思います。

想定している読者は、Prototypeライブラリをただ使うだけでなく、やっていることを理解したいという方、使われているコードを応用して自分なりの改造をしてみたい方、などです。

今回の連載では執筆開始時点の最新版であるバージョン1.5.1.1を対象としています。現在1.6.0rcが準備されていますが、説明の都合上行番号に大きく依存しているため、最後までバージョン1.5.1.1を対象とすることになります。

Prototypeライブラリの構成

名前空間

このライブラリでは、グローバルな名前空間のうち、以下のものを使っています。

名前実体
Prototypeオブジェクト
Classオブジェクト
Abstractオブジェクト
Tryオブジェクト
PeriodicalExecuterPrototypeライブラリ式クラス
TemplatePrototypeライブラリ式クラス
$break空オブジェクト
$continueError形のインスタンス、後方互換性のため
Enumerableオブジェクト
$AArray.from関数オブジェクトの別名
$w関数オブジェクト
Hashコンストラクタとしての関数オブジェクト
$H関数オブジェクト
ObjectRangePrototypeライブラリ式クラス
$R関数オブジェクト
Ajaxオブジェクト
$関数オブジェクト
Elementオブジェクト(元々ブラウザが持っていない場合に作成)
Toggleオブジェクト
Insertionオブジェクト
SelectorPrototypeライブラリ式クラス
$$関数オブジェクト
Formオブジェクト
Fieldオブジェクト。Form.Elementの別名
$F関数オブジェクト。Form.Element.Methods.getValueの別名
Eventオブジェクト(元々ブラウザが持っていない場合に作成)
Positionオブジェクト

他のライブラリや、自分のコードと共存させるときは、これらの名前と衝突しないように気をつける必要があります。同じ名前を使ってしまうと、コードがロードされた順番によって挙動が変わる、というようなわかりにくい問題が発生してしまう場合が出てきます。

オブジェクト、クラスの使われ方

ライブラリ内では、上記の名前空間は大きく分けて以下のような使われ形をしています。

  1. その下に別のオブジェクトを入れるための親名前空間として使う
  2. Class.create()を使って Prototypeライブラリ風のクラスとして定義する
  3. Object.extend()を使って他のクラス、オブジェクトから継承されることを前提とする関数を集める
  4. コードを簡潔に記述する為に短い名前の関数として使う

特に、Object.extend()を使って継承を実現している箇所が多く、最終的にどのオブジェクト・クラスにどのメソッドが定義されているのかがわかりにくい所があります。

コードを追っていく場合は、その名前のオブジェクトの定義部分だけではなく、Object.extend()で拡張されている部分も追っていく必要があります。

既存オブジェクトへの拡張

Prototypeライブラリの特徴の一つとして、既存のオブジェクトへの拡張を行っているというものがあります。

大きくはElementやArray、Stringに対して多数のメソッドを追加していますし、Function、Object、Number、Dateにもいくつかメソッドが追加されています。

これらの追加メソッドは便利なものが多いのですが、副作用に気をつける必要があります。

JavaScriptでコード上でプロパティとして追加されたメソッドには、属性として DontEnum属性を追加することができません。そのため、拡張されたオブジェクトのインスタンスを作成しfor (var i in obj)などでプロパティを列挙すると、他の組み込みメソッドなどとは異なり通常のプロパティとして列挙されるプロパティに含まれるようになってしまいます。

このことを忘れていると、⁠自分が追加していないプロパティがなぜか含まれている!」ということになり、デバッグに手間取ることになりがちです。

var a = new Array;
a.push(100);
a.push(200);
for (var i in a) {
  console.log(i);
}

というコードでは、配列内の2要素だけが列挙されることを期待したいのですが、Prototypeライブラリをロードした後では、Array.prototypeにメソッドがプロパティとして追加されていますので、0、1というインデックス値のプロパティ以外にも、それらのメソッドが列挙されてしまいます。

この場合は、例えばArrayならfor (var i = 0; i < a.length; i++) { ... }という形で列挙するか、Array.each()などのイテレータメソッドを使う、という形にする必要があります。

先頭のコメント、著作権表示

0001: /*  Prototype JavaScript framework, version 1.5.1.1
0002:  *  (c) 2005-2007 Sam Stephenson
0003:  *
0004:  *  Prototype is freely distributable under the terms of an MIT-style license.
0005:  *  For details, see the Prototype web site: http://www.prototypejs.org/
0006:  *
0007: /*--------------------------------------------------------------------------*/
0008: 

7行目まではコメントです。PrototypeライブラリはMITスタイルのライセンスであることが宣言されています。 このライセンスであるおかげで、商用製品であろうとも利用が可能となっていて、普及が促進されたという側面もあるでしょう。

Prototype オブジェクト

0009: var Prototype = {
0010:   Version: '1.5.1.1',
0011: 
0012:   Browser: {
0013:     IE:     !!(window.attachEvent && !window.opera),
0014:     Opera:  !!window.opera,
0015:     WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
0016:     Gecko:  navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1
0017:   },
0018: 
0019:   BrowserFeatures: {
0020:     XPath: !!document.evaluate,
0021:     ElementExtensions: !!window.HTMLElement,
0022:     SpecificElementExtensions:
0023:       (document.createElement('div').__proto__ !==
0024:        document.createElement('form').__proto__)
0025:   },
0026: 
0027:   ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',
0028:   JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,
0029: 
0030:   emptyFunction: function() { },
0031:   K: function(x) { return x }
0032: }
0033: 

まず、Prototypeという名前のオブジェクトが生成されています。

以前はVersion、Kという属性くらいしかなかったのですが、現在ではVersion、Browser、BrowserFeatures、ScriptFragment、JSONFilter、emptyFunction、Kというプロパティが定義されています。

ドキュメント化されていなかったこともあり、このオブジェクト内にあるものは、どちらかというとprototype.js内部で使われることが多かったかと思いますが、現在では一部がドキュメント化されています。

9行目において、var Prototype = { ... } という形でリテラルオブジェクトとして生成されています。このPrototypeオブジェクトは、名前空間(namespace)として使うために生成されている、とのことなので、単にPrototypeライブラリ全体に関わる値を置いておくための場所、と考えればいいでしょう。

10行目ではPrototype.Versionを定義しています。これは文字列で、現バージョンでは '1.5.1.1' となっています。文字列形式だと数値比較がしづらかったりしますが、例えばscript.aculo.usでは以下のようにしてprototype.jsの古いバージョンを検出しています。

if (parseFloat(Prototype.Version.split(".")[0] + "." +
               Prototype.Version.split(".")[1]) < 1.5) {
  throw("script.aculo.us requires the Prototype JavaScript framework >= 1.5.0");
}

Prototypeライブラリの特定のバージョン以降を要求するような場合には、このような書き方をすればいいでしょう。

12行目からはPrototype.Browserオブジェクトの定義です。中身はいくつかのブラウザを判別するためのboolean値が入るようになっています。

例えばif (Prototype.Browser.IE) { ... } などとすることでIE環境で動作しているかどうかの確認が行えます。ここで、Prototype.Browser.IEではattachEvent()関数の存在を確認しつつ、Operaを除外するような定義になっています。

Prototype.Browser.Operaでは単純に単純にwindow.operaプロパティの存在を確認しているだけです。Prototype.Browser.WebKitではUserAgent文字列から 'AppleWebKit/' という文字列が存在するかどうかを確認しています。Safariだけではなく、WebKitを使った派生ブラウザ全体のための真偽値ですね。

Prototype.Browser.GeckoではUserAgent文字列に'Gecko'を含み、かつ'KHTML'を含まない、という条件になっています。これはSafariが'(KHTML,like Gecko)'という紛らわしい文字列をUserAgentに含んでいるためです。

19行目からはPrototype.BrowserFeaturesというオブジェクトです。ここではXPath、ElementExtensions、SpecificElementExtensionsという真偽値プロパティで、各機能が使えるかどうかをチェックできるようになっています。

Prototype.BrowserFeatures.XPathでは、document.evaluate関数が存在するかどうかを確認しています。これはDOM 3 XPathで定義されているインターフェイスで、Mozilla 系、Opera 9、Safari 3(ベータ版で確認)で実装されています(IEは7でもダメ⁠⁠。この真偽値によりDOM 3 XPathが使えるのかどうかがわかります。

Prototype.BrowserFeatures.ElementExtensions では、window.HTMLElementプロパティが存在しているかどうかを見ています。これは、DOM 2 HTMLのHTMLElementインターフェイスが実装されているかどうかを調べています。IE系は実装しておらず、Mozilla、Opera、Safariなどでは実装されています。ここを見て、Prorotypeライブラリ側でメソッドを提供する必要があるのか、ネイティブのメソッドを呼び出すことができるのかを判別しています。

Prototype.BrowserFeatures.SpecialElementExtensionsでは、div要素、form要素が個別のプロトタイプを持っているかどうかを調べています。!==演算子なので、異なるプロトタイプを持っていたらSpecialElementExtensionsは真となります。

真の場合、window.HTMLDivElement、window.HTMLFormElementインターフェイスを個別に持っていることになるので、後でElement.Methods.ByTagテーブルによって要素が拡張される際にそれらが利用されます。

27行目では、Prototype.ScriptFragmentとして正規表現で使うための文字列を定義しています。この文字列は、Stringオブジェクトの拡張部分で、script要素を取り除いたり、中身を取り出したりするために使われています。

28行目では、Prototype.JSONFilterとして、JSON文字列をフィルタするための正規表現オブジェクトを定義しています。単純にJSONを返すものをHTTPで取得できる場所に置いておくと、想定外の利用をされる可能性がありますが、それを避けるための目的で/* ... */で括った状態でJSONテキストを配置することがあります。Prototypeライブラリではこのときに/*-secure-JSON文字列*/ という形で置いておくと、自動的に外側の文字列を削って、適切なJavaScriptオブジェクトとして返す、ということをしてくれます。

30行目は何もしない関数Prototype.emptyFunction()です。これは関数が省略可能な引数として渡される際などに、デフォルトの値として使われます。以下の例では、state == 'Complete'として、this.options['onComplete']に関数が指定されていればそれを呼び出し、無ければ何もしないemptyFunction()を呼び出しています。このようにコードをシンプルに記述するために使われています。

(this.options['on' + state] || Prototype.emptyFunction)(transport, json);

31行目も似たような関数で、渡された引数をそのまま返す関数です。こちらはフィルタ系のコードでデフォルトの関数として使われていて、何も行いたい処理が無い場合にはこの関数を使って単に値を返すという形で記述されています。

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ライブラリ全体で使われており、使用頻度の高い物となっています。

Object オブジェクトへの拡張

0051: Object.extend(Object, {
0052:   inspect: function(object) {
0053:     try {
0054:       if (object === undefined) return 'undefined';
0055:       if (object === null) return 'null';
0056:       return object.inspect ? object.inspect() : object.toString();
0057:     } catch (e) {
0058:       if (e instanceof RangeError) return '...';
0059:       throw e;
0060:     }
0061:   },
0062: 
0063:   toJSON: function(object) {
0064:     var type = typeof object;
0065:     switch(type) {
0066:       case 'undefined':
0067:       case 'function':
0068:       case 'unknown': return;
0069:       case 'boolean': return object.toString();
0070:     }
0071:     if (object === null) return 'null';
0072:     if (object.toJSON) return object.toJSON();
0073:     if (object.ownerDocument === document) return;
0074:     var results = [];
0075:     for (var property in object) {
0076:       var value = Object.toJSON(object[property]);
0077:       if (value !== undefined)
0078:         results.push(property.toJSON() + ': ' + value);
0079:     }
0080:     return '{' + results.join(', ') + '}';
0081:   },
0082: 
0083:   keys: function(object) {
0084:     var keys = [];
0085:     for (var property in object)
0086:       keys.push(property);
0087:     return keys;
0088:   },
0089: 
0090:   values: function(object) {
0091:     var values = [];
0092:     for (var property in object)
0093:       values.push(object[property]);
0094:     return values;
0095:   },
0096: 
0097:   clone: function(object) {
0098:     return Object.extend({}, object);
0099:   }
0100: });
0101: 

先程定義したObject.extend()関数を使って、ECMA-232で定義されたObjectオブジェクトを拡張しています。形式はObject.extend()の例と同様ですが、第二引数に渡しているオブジェクトリテラルが大きいのでちょっと把握しづらいかもしれません。

ここではObjectオブジェクトに対して、inspect、toJSON、keys、values、cloneというプロパティを定義しており、各々には関数オブジェクトが代入されています。

52行目からのinspect()は、デバッグ用にオブジェクトの文字列表記を返すメソッドです。Prototypeライブラリの中では、String、Array、Enumerable、Hashというクラス内でinspect()メソッドを上書きしており、その場合はそちらの関数が使われます。

まず54行目で、===演算子を使ってundefinedかどうかを確認しています。undefined値は、nullとは異なる値で、変数に値が代入されていない時に用いられます。等しければ'undefined'という文字列を返します。

55行目ではnull値と比較しています。こちらも等しければ'null'という文字列を返します。

56行目では、対象となるオブジェクトにinspectプロパティがある場合は、それを関数とみなして呼び出し、そうでなければtoString()メソッドがあることを仮定して呼び出します。基本的には全てのオブジェクトにはtoString()メソッドが存在するはずなので、それが適切な文字列を返すようになっています。

複雑な型の場合は、独自にinspect()を実装して、適切な文字列を返すようにすることが想定されているようです。

これらの三行での挙動が、try {}で括られています。ただし、catchされているのは例外がRangeErrorだった場合のみであり、その場合は'...'が返されます。なぜここでRangeErrorだけをcatchしているのかは不明です。

63行目からはオブジェクトをJSON化するためのtoJSON()メソッドの定義です。

まず、引数として渡されたオブジェクトの型をtypeof演算子で取得します。65行目からのswitch文で、undefined、function、unknownの場合は何も返さずに帰ります。booleanの場合にはtoString()メソッドの返り値を返しています。

ここで、switch文の中で、単にreturnとしている文とreturn object.toString()と値を明示的に返している文が混在しています。単にreturnだけを書いた場合、undefined を返す、ということは仕様として定義されているのですが、この書き方をしてしまうとぱっと見で関数の呼出し側にはどんな値が返るのかがわからなくなってしまうので、何かしら明示的に返すように記述した方がいいでしょう。FirefoxなどでJavaScriptの警告を厳格にすると、function ... does not always return a valueという警告が出力されてしまいます。

  • ECMA-262 第三版 - 12.9 The return statement

83行目からはkeys()メソッドです。オブジェクトが保持しているプロパティのうち、DontEnum属性が付いていないものを列挙して、文字列の配列として返します。

コード上は特に癖もなく、for (variable in object) でループさせて、事前に準備した配列keysにpush()していきそれをreturnで返す、となっています。

90行目から、values()メソッドです。これは他の言語でもkeys()と対で提供されていることが多いでしょう。keys()がプロパティ名を配列に入れているのに対して、values()ではプロパティ値を配列に入れ、それを返しています。

97行目からはclone()メソッドです。単純にオブジェクトの(浅い)コピーを作る関数です。

JavaScriptにおいて、native object以外は代入演算子でコピーされるものは参照渡しになります。オブジェクトの複製を作りたい場合にはこの関数を使うといいでしょう。

Functionオブジェクトへの拡張

0102: Function.prototype.bind = function() {
0103:   var __method = this, args = $A(arguments), object = args.shift();
0104:   return function() {
0105:     return __method.apply(object, args.concat($A(arguments)));
0106:   }
0107: }
0108: 

Functionオブジェクトのbind()メソッドです。これはFunction.prototype.bindへの代入となっているので、全ての関数に対してbind()メソッドが使えるようになります。

通常のオブジェクトに対するメソッド呼び出しでは、thisがそのオブジェクト自体への参照となりますが、bind()を使うことでその関数内でのthisを指定することができるようになります。

ここでは、関数が呼び出されるタイミングで、bind()呼出し時に渡されたオブジェクトを参照するために、クロージャが使われています。

まず、103行目で__methodに代入されているのは、bind()を呼び出された元の関数オブジェクトです。argsにはbind()呼出し時の引数全体が配列として入ります。objectにはその先頭のオブジェクトがshift()で取り出されて入ります。これが後でthisとして扱われるオブジェクトとなります。

104行目で関数が定義され、bind()の返り値として関数オブジェクトが返るようになっています。ここで、元のbind()関数内のローカル変数である__method, args, objectを105行目のfunction(){ ... }で参照することにより、bind()で返される関数オブジェクトの実行コンテキストにこれらのローカル変数が束縛されます。

bind()の呼び出し自体が終わると、ローカル変数である__methodなどに直接アクセスすることはできなくなりますが、returnで返された関数オブジェクトの中からは引き続き__method, args, objectを参照することができるようになっています。

105行目で使われているargumentsは、返された関数が呼び出される時に渡された引数です。なので、最終的に大元の関数が呼び出される際には、

  1. thisはbind()で指定された最初の引数
  2. arguments(引数の配列)は、bind()で指定された2番目以降の引数に、返された関数の呼出し時に渡された引数を加えたもの

となります。

0109: Function.prototype.bindAsEventListener = function(object) {
0110:   var __method = this, args = $A(arguments), object = args.shift();
0111:   return function(event) {
0112:     return __method.apply(object, [event || window.event].concat(args));
0113:   }
0114: }
0115: 

やっていることはFunction.bind()とほぼ同様です。

違いは、最終的に関数が呼び出される時の引数が、Eventオブジェクトとなる点です。

関数が呼び出される際には、arguments(引数の配列)の最初がEventオブジェクトで、それ以降はbindAsEventListener()を呼び出した際の2番目以降が渡されます。

Number オブジェクトへの拡張

0116: Object.extend(Number.prototype, {
0117:   toColorPart: function() {
0118:     return this.toPaddedString(2, 16);
0119:   },
0120: 
0121:   succ: function() {
0122:     return this + 1;
0123:   },
0124: 
0125:   times: function(iterator) {
0126:     $R(0, this, true).each(iterator);
0127:     return this;
0128:   },
0129: 
0130:   toPaddedString: function(length, radix) {
0131:     var string = this.toString(radix || 10);
0132:     return '0'.times(length - string.length) + string;
0133:   },
0134: 
0135:   toJSON: function() {
0136:     return isFinite(this) ? this.toString() : 'null';
0137:   }
0138: });
0139: 

Object.extend()を使って、JavaScript native objectであるNumberを拡張しています。これもNumber.prototypeを書換えているので、Number全体に作用します。

以下、Numberオブジェクトに関する説明中では、Numberオブジェクト自体が指し示す数値をnと表記します。

117行目からはNumber.toColorPart()メソッドを定義しています。これは後述するtoPaddedStringを使って、nを16進数で2桁の数字の文字列として返します。

CSSなどで、#aabbcc形式の色指定文字列を作成するときに使います。

121行目からは succ()メソッドです。これはsuccessorの略のようで、nの整数における次の数値( すなわち+1したもの)を返します。それほど便利な関数ではないように思えますが、ObjectRangeで使うためにあるようです。

同名のメソッドがStringにも追加されています。

125行目からは times()メソッドです。これは$R()関数を使い、0からn-1までの回数(要するにn回)iteratorを呼び出します。呼び出されるiteratorは関数オブジェクトで、その関数には0からn-1までの数が引数として渡されます。

130行目からはtoPaddingSring()メソッドです。数値を文字列に変換しますが、基数を変換してくれることとと、指定した長さに満たない場合は'0'を足してくれる、という機能があります。

まず131行目でnをradixを基とする数値表現文字列に変換しています。(radix || 10)としているので、radixが0または指定されていない場合には10を指定したのと同じことになります。

その文字列に対して、'0'.times(length - string.length)という形で、318行目で定義されているString.times()を使って必要なだけ'0'という文字を繰り返した文字列を作ります。

最終的にこの'0'...に131行目で作ったstringを接続して返します。

135行目からはtoJSON()メソッドです。まずisFinite()を呼び出し、NaNかどうかを確認しています。もしNaNならJSONとして扱いやすいように'null'という文字列を返し、そうでなければ数値をtoString()した結果を返します。

JavaScriptに存在するNaNをわざわざ'null'に変換してJSON(JavaScript Object Notation)とする、というのも変な話のようにも見えますが、RFC4627などでの定義のなかで、NaNやInfinityは使うことができない、となっているので、toJSON()が返す文字列としては'null'にするしか無いようです。

Dateオブジェクトへの拡張

0140: Date.prototype.toJSON = function() {
0141:   return '"' + this.getFullYear() + '-' +
0142:     (this.getMonth() + 1).toPaddedString(2) + '-' +
0143:     this.getDate().toPaddedString(2) + 'T' +
0144:     this.getHours().toPaddedString(2) + ':' +
0145:     this.getMinutes().toPaddedString(2) + ':' +
0146:     this.getSeconds().toPaddedString(2) + '"';
0147: };
0148: 

native objectであるDateを拡張して、toJSON()メソッドを追加しています。

記述が長いのでわかりにくいのですが、"2007-07-03T20:13:15" という表記の文字列を返します(二重引用符も含む⁠⁠。桁数を揃えるためにNumber.toPaddidString()を使っています。

Tryオブジェクト

0149: var Try = {
0150:   these: function() {
0151:     var returnValue;
0152: 
0153:     for (var i = 0, length = arguments.length; i < length; i++) {
0154:       var lambda = arguments[i];
0155:       try {
0156:         returnValue = lambda();
0157:         break;
0158:       } catch (e) {}
0159:     }
0160: 
0161:     return returnValue;
0162:   }
0163: }
0164: 

Tryオブジェクトを定義していますが、現状では使われているのはTry.these()関数だけです。

Try.these()関数は、引数として任意の数の関数オブジェクトを受け取ります。任意の数の引数を受け取れるようにするために、150行目の関数定義では引数を明示せず、153行目のfor文でarguments配列をひとつずつ処理しています。

引数の先頭から処理していき、その関数オブジェクトの呼び出し(156行目)が例外を発生させずに終了できれば、breakでfor文を抜け、その関数の返り値returnValueをTry.these()の呼出し元に返します。

例外が発生した場合、特にその例外に対しては何もせず(158行目のからcatch()節⁠⁠、次の引数の関数オブジェクトを試します。

今のところ、Prototypeライブラリ内ではこの関数はAjaxオブジェクトのXMLHttpRequest、ActiveXObjectを取得する際にのみ使われています。

PeriodicalExecuterクラス

0165: /*--------------------------------------------------------------------------*/
0166: 
0167: var PeriodicalExecuter = Class.create();
0168: PeriodicalExecuter.prototype = {
0169:   initialize: function(callback, frequency) {
0170:     this.callback = callback;
0171:     this.frequency = frequency;
0172:     this.currentlyExecuting = false;
0173: 
0174:     this.registerCallback();
0175:   },
0176: 
0177:   registerCallback: function() {
0178:     this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
0179:   },
0180: 
0181:   stop: function() {
0182:     if (!this.timer) return;
0183:     clearInterval(this.timer);
0184:     this.timer = null;
0185:   },
0186: 
0187:   onTimerEvent: function() {
0188:     if (!this.currentlyExecuting) {
0189:       try {
0190:         this.currentlyExecuting = true;
0191:         this.callback(this);
0192:       } finally {
0193:         this.currentlyExecuting = false;
0194:       }
0195:     }
0196:   }
0197: }

PeriodicalExecuterクラスです。Class.create()を使って雛型を作り、168行目でprototypeプロパティを上書きすることで各メソッドを定義しています。

169行目からがコンストラクタです。メンバー変数としてcallback、frequencyを設定し、currentlyExecutingフラグをfalseで初期化します。最後にregisterCallback()を呼んで、最初のタイマーを登録します。

177行目からがそのregisterCallback()メソッドで、setInterval()を使って周期的なタイマーを設定しています。ここでthis.onTimerEvent.bind(this)は定型句のようなもので、自分のonTimerEventメソッドが、thisが現在と同じ自分自身を指した状態で呼ばれるようにしています。this.frequencyは1000倍しているので、PeriodicalExecuterの生成時に渡すfrequencyの値の単位は秒となります。

先に 187行目のonTimerEvent()メソッドを確認しましょう。ここでは、一番外側でthis.currentlyExecutingフラグを見て、falseだった時のみ内側のコードが実行されるようになっています。

そして、try句の中でいったんthis.currentlyExecutingをtrueにした状態でthis.callback()を呼び出し、それが終わるとfinally節で再度this.currentlyExecutingをfalseに戻します。

こうすることによって、時間がどれくらいかかるのかわからないcallback関数を呼び出している間はcurrentlyExecutingがtrueになるようになり、その実行中に再度setInterval()の割り込みがあった場合でも並列して多重に実行されることが無いようになっています。

逆にいうと、たとえfrequencyを1秒に設定したとしても、60秒で確実に60回呼び出されるわけではなく、callback関数の費す時間によっては少ない回数しか呼び出されない可能性があります。

最後に181行目のstop()メソッドで、タイマーを停止します。178行目でthis.timerを保存していれば、そのタイマーIDを使ってclearInterval()を呼び出してタイマーを停止し、再度同じことをしないようにthis.timerをnullとして後片付けをします。

おすすめ記事

記事・ニュース一覧