prototype.jsを読んでみる (1)
prototype.jsを頭から地道に読んでます。
AJAX周りに入る直前までしか読み終わっていませんが、これはなかなか。
[注意]
prototype.jsは1.4.0_rc2を使ってますが、この文章には一部、1.3.1のコードが混じっています。
2005-1-15
長いので、本文を追記に移動
ついでにカテゴリをJavaScriptに変更
var Prototype = {
Version: '1.4.0_rc2',emptyFunction: function() {},
K: function(x) {return x}
}
Prototype.Versionでprototype.jsのバージョンが取得できる。
emptyFunctionは何もしない関数のオブジェクト。
alert = Prototype.emptyFunction;とかすると、alert()を呼び出しても何も起きなくなる。
alert = function() {};としても同じことなので、なくても困らないが、あるとちょっとだけ便利。
Kは引数をそのまま返すだけの関数。
関数型プログラミングするとき、何もしない関数が欲しくなるケースがあるけど、そんなときに使うもの?
たぶん、lisp界隈の用語なんだろうけど、調べ切れませんでした。
var Class = {
create: function() {
return function() {
this.initialize.apply(this, arguments);
}
}
}
argumentsは常に関数に渡る値で、Argumentsのインスタンス。全ての引数が配列として入る。
apply()はFunctionが持っている関数で、任意のオブジェクトに対して関数を呼び出せる。
this.initialize.apply(this, arguments)はthis.initialize(arguments[0], arguments[1], ..., arguments[arguments.length - 1])と同じ意味。
使い方はこんな感じ。
var Alerter = Class.create(); // Alerterにinitialize()を呼び出す関数オブジェクトをbindする
Alerter.prototype = { // (関数)オブジェクトに色々足す
message: null,
initialize: function(message) {
this.message = message;
},
print: function() {
alert(message);
}
};
var alerter = new Alerter("bar"); // Alerterにbindされている関数オブジェクトを実行する
// → initializeが呼び出され、Alerterのコピーが生成される
alerter.print();
var Abstract = new Object();
C++で言うところの抽象基底クラス。
Objectがその役割をするのがJavaScript本来の姿だが、prototype.jsがObjectを拡張して関数を追加しちゃったりしているので、素のObjectが欲しいときに使う。
Object.extend = function(destination, source) {
for (property in source) {
destination[property] = source[property];
}
return destination;
}
継承を実現するために追加された関数。
sourceにあるプロパティを全てdestinationにコピーする。
使い方はこんなの。
var Person = Class.create();
Person.prototype = {
name: null,
address: null,
initialize: function(name, address) {
this.name = name;
this.address = address;
}
};
var Student = Class.create();
Object.extend(Student.prototype, Person.prototype); // StudentはPersonの一種
Object.extend(Student.prototype, { // 継承した場合は、extendを使ってprototypeを書き換える / 直接代入してしまうと、継承したものが全部消える
initialize: function(name, address) {
Person.prototype.initialize.apply(this, arguments); // 必要なら親のコンストラクタを呼び出す
}
});
Object.inspect = function(object) {
try {
if (object == undefined) return 'undefined';
if (object == null) return 'null';
return object.inspect ? object.inspect() : object.toString();
} catch (e) {
if (e instanceof RangeError) return '...';
throw e;
}
}
与えられたオブジェクトのinspect()を呼び出し、実装されていなければtoString()を呼び出す。オブジェクトの直列化に使う。
呼び出されるinspect()は各オブジェクトが実装するが、基本的なオブジェクトに関してはprototype.jsがデフォルトの実装を提供している。
var Alerter = Class.create();
Alerter.prototype = {
messages: null,
initialize: function() {
if (arguments.length) {
message = [];
for (var i = 0; i < arguments.length; i++) {
message.push(arguments[0]);
}
}
},
print: function() {
for (var i = 0; i < messages.length; i++) {
alert(messages[i]);
}
},
inspect: function() {
var result = "Alerter(message:";
if (messages.length == 0) {
result += "null";
} else if (messages.length == 1) {
result += "'" + messages[0] + "'";
} else {
result += "['" + messages[0] + "'";
for (var i = 1; i < messages.length; i++) {
result += ",'" + messages[i] + "'";
}
result += "]";
}
return result + ")";
}
};
var alerter0 = new Alerter();
var alerter1 = new Alerter("abc");
var alerter2 = new Alerter("abc", "def");
alert(Object.inspect(alerter0));
alert(Object.inspect(alerter1));
alert(Object.inspect(alerter2));
実行結果
Alerter(message:null)
Alerter(message:'abc')
Alerter(message:['abc','def'])
Function.prototype.bind = function(object) {
var __method = this;
return function() {
return __method.apply(object, arguments);
}
}
C++のstd::bind()みたいなやつをFunctionに追加する。要はclosureを作る関数(?)。
var Timer = Class.create();
Timer.prototype = {
message: null,
initialize: function(message) {
this.message = message;
}
onTimedOut: function() {
alert("time out:" + message);
}
};
var timer = new Timer("hogehoge");
setInterval(timer.onTimedOut.bind(timer), 1000);
といった形で使うと便利。。
普通にsetInterval(timer.onTimedOut, 1000)とすると、timer.onTimedOutが呼ばれたときはthisにwindowが入っているのでエラーになる。
そういうときにbindを使うと、thisにonTimedOut()を実装しているオブジェクト(=timer)が入るので、普通のイベントハンドラっぽく書ける。
従来はグローバル変数を使って解決していた部分ですが、prototype.jsできれいに書けるようになるいい例。
Function.prototype.bindAsEventListener = function(object) {
var __method = this;
return function(event) {
return __method.call(object, event || window.event);
}
}
基本的にはbind()と同じですが、IEとMozillaのイベントの違いを吸収するコードが次の一行。
return __method.call(object, event || window.event);
IEだとeventがnullになり、window.eventに値が入るので、こうしておくとどちらを使ってもイベントハンドラの第一引数にeventが入るようになる。
Object.extend(Number.prototype, {
toColorPart: function() {
var digits = this.toString(16);
if (this < 16) return '0' + digits;
return digits;
},succ: function() {
return this + 1;
},times: function(iterator) {
$R(0, this, true).each(iterator);
return this;
}
});
組み込みオブジェクトのNumberに関数を追加している。継承せずにこんなことができるんだから、prototype型言語はいいなぁ。
toColorPart()は16進表記の文字列を作る関数。Number.toString()って基数を指定できたのか。
(142).toColorPart()とすると、文字列"8e"が返ってくる。RGBを扱うためにあるっぽい。
ちなみに142.toColorPart()だと、「小数だと思ってパースしたら数字以外のものがあるぞ」エラーになる。
succ()は1を足した値を返すだけ。使い道が謎。反復子用のインターフェース?
times()は分かり難い。0~(this - 1)までの数列に対して、iteratorで指定される関数を適用するものだけど、コードを追うのが大変。
iteratorでcontinueやbreak;を実行する手順がちょっと面白い。
例外$continueをthrowすると次の繰り返しに飛び、$breakだと中断するというインターフェース。
var array = [0, 1, 2, 3, 4, 5];
for (i in array) {
if (isBreak(array[i])) {
break;
} else if (isNext(array[i])) {
continute;
}
}
をtimes()で書くと、
Number(6).times(function(i) {
if (isBreak(i)) {
throw $break;
} else if (isNext(i)) {
throw $continute;
}
});
となる。普通の例外はtimes()の外まで飛ぶので、その辺も同じように扱える。
var Try = {
these: function() {
var returnValue;for (var i = 0; i < arguments.length; i++) {
var lambda = arguments[i];
try {
returnValue = lambda();
break;
} catch (e) {}
}return returnValue;
}
}
関数を呼び出してみて、成功したらそこで値を返し、失敗したら次の関数を試す関数。
prototype.js内で実際に使っているのはこんなの。
var Ajax = {
getTransport: function() {
return Try.these(
function() {return new ActiveXObject('Msxml2.XMLHTTP')},
function() {return new ActiveXObject('Microsoft.XMLHTTP')},
function() {return new XMLHttpRequest()}
) || false;
},
activeRequestCount: 0
}
Try.these()に三つの関数を渡してみて、成功したものの値を使う。なるほど。
var PeriodicalExecuter = Class.create();
PeriodicalExecuter.prototype = {
initialize: function(callback, frequency) {
this.callback = callback;
this.frequency = frequency;
this.currentlyExecuting = false;this.registerCallback();
},registerCallback: function() {
setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
},onTimerEvent: function() {
if (!this.currentlyExecuting) {
try {
this.currentlyExecuting = true;
this.callback();
} finally {
this.currentlyExecuting = false;
}
}
}
}
タイマーを実現するオブジェクト。
定期的に実行する関数と周期(秒単位)を指定してオブジェクトを生成すれば、定期的に実行されるようになる。
PeriodicalExecuterオブジェクトをbind()した関数をsetInterval()に渡すことでグローバル変数を避けていること、二重実行されないようにガードを入れている(currentlyExecuting)のがポイント。
catchなしのtry~finallyも有効なテクニック。
使い方はこんな感じ。
function alertMessage() {
alert("Foo");
}
var periodicalAleter = new PeriodicalExecuter(alertMessage, 60); // 1分ごとに警告を出す
タイマーを開始したが最後、止める手段がないのは色々とまずい気がする。
function $() {
var elements = new Array();for (var i = 0; i < arguments.length; i++) {
var element = arguments[i];
if (typeof element == 'string')
element = document.getElementById(element);if (arguments.length == 1)
return element;elements.push(element);
}return elements;
}
$をシンボルに使えるのか、というのが一番のポイントかも。他は見たまんま。
getElementById()の省略記法を提供するのが目的だと思うが、複数のIDを指定できるようになっている。
var elems = [];
var array = ["Foo", "Bar", "Baz"];
for (var id in array) {
var e = document.getElementById(array[id]);
if (e) {
elems.push(e);
}
}
と書いていたのが、
var elems = $("Foo", "Bar", "Baz");
と、一行で書けるようになる。
IDを一つしか渡さなかった場合は、配列ではなく要素が返るので、下の二行は同じ意味になる。
var elem = document.getElementById("Foo");
var elem = $("Foo");
// 1.3.1にしかないコード
if (!Array.prototype.push) {
Array.prototype.push = function() {
var startLength = this.length;
for (var i = 0; i < arguments.length; i++)
this[startLength + i] = arguments[i];
return this.length;
}
}
須崎さんのblog(2004/12/22 IE5.01をサポートするために)と同じ。
複数の引数があった場合に対応している分、こっちのが高機能ですが。
Array.push()が使えない環境があるので、それに対応するためにArrayのprototypeをいじってpush()を追加している。
// 1.3.1にしかないコード
if (!Function.prototype.apply) {
// Based on code from http://www.youngpup.net/
Function.prototype.apply = function(object, parameters) {
var parameterStrings = new Array();
if (!object) object = window;
if (!parameters) parameters = new Array();for (var i = 0; i < parameters.length; i++)
parameterStrings[i] = 'parameters[' + i + ']';object.__apply__ = this;
var result = eval('object.__apply__(' +
parameterStrings.join(', ') + ')');
object.__apply__ = null;return result;
}
}
これもFunction.apply()がない環境のためにprototypeをいじっている。
apply()自体がmeta programing寄りの機能なので、eval()の使い方のお手本みたいなコードになっている。
まず、真ん中のforループで、"parameters[0], parameters[1], parameters[2]"といった文字列を作っている。
object.__apply__ = thisは、object.__apply__に適用される関数自身(=this)を代入している。
こうすることで、例えばobject.__apply__(parameters[0], parameters[1], parameters[2])がeval()経由で実行される。
Object.extend(String.prototype, {
stripTags: function() {
return this.replace(/<\/?[^>]+>/gi, '');
},escapeHTML: function() {
var div = document.createElement('div');
var text = document.createTextNode(this);
div.appendChild(text);
return div.innerHTML;
},unescapeHTML: function() {
var div = document.createElement('div');
div.innerHTML = this.stripTags();
return div.childNodes[0] ? div.childNodes[0].nodeValue : '';
},toQueryParams: function() {
var pairs = this.match(/^\??(.*)$/)[1].split('&');
return pairs.inject({}, function(params, pairString) {
var pair = pairString.split('=');
params[pair[0]] = pair[1];
return params;
});
},toArray: function() {
return this.split('');
},camelize: function() {
var oStringList = this.split('-');
if (oStringList.length == 1) return oStringList[0];var camelizedString = this.indexOf('-') == 0
? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1)
: oStringList[0];for (var i = 1, len = oStringList.length; i < len; i++) {
var s = oStringList[i];
camelizedString += s.charAt(0).toUpperCase() + s.substring(1);
}return camelizedString;
},
inspect: function() {
return "'" + this.replace('\\', '\\\\').replace("'", '\\\'') + "'";
}
});String.prototype.parseQuery = String.prototype.toQueryParams;
組み込みの文字列オブジェクトを拡張している。Object.extend()の第二引数にハッシュを指定する形式は、この後、たくさん出てくる。
stripTags()
正規表現を使って、全てのタグを消去している。
escapeHTML()
文字列にHTMLとして不正な文字が含まれている場合でも、適切にエスケープする。
普通、正規表現とかで置換するところだが、文字列をDOMのtext-nodeとしてつっこんだ後、innerHTMLで取得している。
エスケープの規則はブラウザが知っている訳だから、そっちに丸投げしたと。うまいけど、パフォーマンスはどうなんだろう。
unescapeHTML()
escapeHTML()の逆で、エスケープされた文字列を復元する。
これまた、innerHTMLでdivの中身をエスケープされた文字列で書き換え、text-nodeの値として取得している。
toQueryParams()
HTTPの問い合わせ文字列(?id=foo&user=anakaみたいな文字列)を、ハッシュにunserializeする。
文字列の?以降(or 先頭から全部)を&で区切った各要素を更に=で区切って、名前と値をペアを得ている。
inject()はprototype.jsがコンテナに追加する関数で、inject()を実装しているコンテナの各要素に第二引数の関数を適用する。
関数には、第一引数にinject()の第一引数、第二引数にコンテナの各要素が渡される。
inject()そのものの返値は第一引数になる。
今回のケースで言えば、第一引数の無名ハッシュに名前と値のペアを順次入れている。
toArray()
文字列を文字(=1文字の文字列)の配列に変換する。
空文字でsplit()してるだけ。
camelize()
ラクダ化。foo-bar-bazみたいな名前(ヘビ)をfooBarBazといった形に変換する。
あっちでもラクダと呼ぶんですな。
'-'を含むフォーム名とオブジェクトの対応付けに使うんだろうな。
処理としては、文字列に'-'が含まれなければ、文字列自身を返す
先頭が'-'だったら('-foo-bar-baz')、先頭も大文字にする(FooBarBaz)
といった辺りが注意するところ。
inspect()
文字列を直列化する。'\'をエスケープした後に"'"をエスケープして、"'"で囲んでいる。
parseQuery()
toQueryParams()の別名。
var $break = new Object();
var $continue = new Object();var Enumerable = {
each: function(iterator) {
var index = 0;
try {
this._each(function(value) {
try {
iterator(value, index++);
} catch (e) {
if (e != $continue) throw e;
}
});
} catch (e) {
if (e != $break) throw e;
}
},all: function(iterator) {
var result = true;
this.each(function(value, index) {
if (!(result &= (iterator || Prototype.K)(value, index)))
throw $break;
});
return result;
},any: function(iterator) {
var result = true;
this.each(function(value, index) {
if (result &= (iterator || Prototype.K)(value, index))
throw $break;
});
return result;
},collect: function(iterator) {
var results = [];
this.each(function(value, index) {
results.push(iterator(value, index));
});
return results;
},detect: function (iterator) {
var result;
this.each(function(value, index) {
if (iterator(value, index)) {
result = value;
throw $break;
}
});
return result;
},findAll: function(iterator) {
var results = [];
this.each(function(value, index) {
if (iterator(value, index))
results.push(value);
});
return results;
},grep: function(pattern, iterator) {
var results = [];
this.each(function(value, index) {
var stringValue = value.toString();
if (stringValue.match(pattern))
results.push((iterator || Prototype.K)(value, index));
})
return results;
},include: function(object) {
var found = false;
this.each(function(value) {
if (value == object) {
found = true;
throw $break;
}
});
return found;
},inject: function(memo, iterator) {
this.each(function(value, index) {
memo = iterator(memo, value, index);
});
return memo;
},invoke: function(method) {
var args = $A(arguments).slice(1);
return this.collect(function(value) {
return value[method].apply(value, args);
});
},max: function(iterator) {
var result;
this.each(function(value, index) {
value = (iterator || Prototype.K)(value, index);
if (value >= (result || value))
result = value;
});
return result;
},min: function(iterator) {
var result;
this.each(function(value, index) {
value = (iterator || Prototype.K)(value, index);
if (value <= (result || value))
result = value;
});
return result;
},partition: function(iterator) {
var trues = [], falses = [];
this.each(function(value, index) {
((iterator || Prototype.K)(value, index) ?
trues : falses).push(value);
});
return [trues, falses];
},pluck: function(property) {
var results = [];
this.each(function(value, index) {
results.push(value[property]);
});
return results;
},reject: function(iterator) {
var results = [];
this.each(function(value, index) {
if (!iterator(value, index))
results.push(value);
});
return results;
},sortBy: function(iterator) {
return this.collect(function(value, index) {
return {value: value, criteria: iterator(value, index)};
}).sort(function(left, right) {
var a = left.criteria, b = right.criteria;
return a < b ? -1 : a > b ? 1 : 0;
}).pluck('value');
},toArray: function() {
return this.collect(Prototype.K);
},zip: function() {
var iterator = Prototype.K, args = $A(arguments);
if (typeof args.last() == 'function')
iterator = args.pop();var collections = [this].concat(args).map($A);
return this.map(function(value, index) {
iterator(value = collections.pluck(index));
return value;
});
},inspect: function() {
return '#<Enumerable:' + this.toArray().inspect() + '>';
}
}
列挙可能であることを示すインターフェースと実装を提供するEnumerable。
Generic Programmingを弱い型の言語でやるとこうなるというお手本。
$break
$continue
each()やall()等の反復子で使う例外用のオブジェクト。
反復子から呼び出される関数(callback的)がループを中断したい場合に、breakやcontinueと同じ意味で使う。
例外仕様をインターフェースの一部として扱うアプローチはなじみ深いけど、このやり方は面白い。
each()
Enumerableなコンテナ(=this)が持っている全ての要素に対して、第一引数で渡される関数を適用する。
関数には第一引数に各要素が、第二引数に要素のインデックスが渡される。
try~catchの中で呼ばれている_each()は、各コンテナが実装すべき「列挙の仕方」。
prototype.js内ではArray, Hash, Range, Ajax.Responders, Element.ClassNamesが実装している。
自分で作ったオブジェクトに実装してEnumerableをObject.extend()すれば、そのオブジェクトに対してeach()を使えるようになる。
all()
each()と同じように、全ての要素に対して第一引数で渡される関数を呼び出す。
each()と違うのは、関数の返値を見て、失敗したらそこで中断する点。
all()自体も、全ての実行に成功したらtrueを、中断したらfalseを返すようになっている。
関数を渡さなければ要素を評価する(Prototype.K()を呼び出す)だけなので、コンテナの中にnullが含まれているかどうかのチェックにも使える。
each()に渡している一時的な関数が、all()のローカル変数を参照しているのもポイント。
any()
all()の逆で、関数が成功したらそこで中断する。
それ以外はall()と同じ。
var foo = ["abc", "def", "ghi"];
if (foo.any(function(value) {return value == "def";})) {
alert("found!");
} else {
alert("not found");
}
とかできる。
collect()
第一引数で渡される関数を各要素に対して呼び出し、その結果を収集する。C++でいうところの、std::transform()。
コンテナの全要素に対して、変更を加えたい場合に使う。
var foo = ["abc", "def", "ghi"];
foo.collect(function(value) {return "***" + value "***";});
とか。
detect()
第一引数で渡される条件(booleanを返す関数)が真になる値をコンテナ中から探す。
var foo = ["a", "bc", "def", "ghij"];
var result = foo.detect(function(value) {return value.length == 3;});
なら"def"が返る。
findAll()
detect()と同じようなものだが、全ての要素を探して配列を返す。
var foo = ["a", "bc", "def", "ghij"];
var result = foo.findAll(function(value) {return value.length < 3;});
なら["a", "bc"]が返る。
grep()
grepする。つまり、正規表現にマッチする要素を探して、配列を得る。
第一引数にパターン、第二引数にマッチした要素に適用する関数を渡す。第二引数を省略すると、マッチした値の集合がそのまま返る。
コンテナの要素がオブジェクトの場合、toString()してからマッチするので注意。
include()
第一引数で指定する値がコンテナに含まれるかどうかを返すだけ。
inject()
第一引数に初期値、第二引数に関数を渡すと、全ての要素に対して関数を適用し、その値を得る。
each()と違うのは、関数の第一引数が繰り返し関数に適用されること。
例えば、
var foo = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var result = foo.inject(1, function(memo, value) {return memo * value;});
とすれば10の階乗が得られる。
invoke()
各要素が持つ任意の関数を呼び出す。collect()のバリエーション。
呼び出す関数名は文字列で指定し、その関数にはinvoke()の第二引数以降が渡される。
$A(arguments).slice(1)は、引数を配列化し、0番目を除いた要素を得る(0番目は関数名が入っている)。
例えばmethodに"foo"が入っている場合、value[method].apply(value, args)はvalue.foo(args)と同じ。
value[method].apply(value, args)で渡しているargsはArrayであってArgumentsではないが、Argumentsとして扱われているように見える。
たぶん、apply()がArray的なインターフェースでのみアクセスしているから。
使い方は、
var array = ["1234", "abcde", "ABCDEF"];
var hexes = array.invoke("substring", 1, 3);
hexes.each(alert);
といった感じ。
max()
各要素の中で最大の値を持つものを返す。
引数に指定した関数を作用させた結果の最大値を得ることもできる。
引数を省略した場合はPrototype.Kが指定されたものと見なして、単に最大値を得る。
if (value >= (result || value))
このif文の条件式はちょっと面白い。
resultに初期値が指定されていない(=undefined)であることを利用して、最初に得たvalueが必ず初期値として採用されるようになっている。
var array = [-4, -3, -2, -1, 0, 1, 2, 3];
var kmax = array.max(); // 3が最大値
var sqMax = array.max(function(value) {return value * value;}); // 16が最大値
min()
最小値を得る以外はmax()と同じ。
partition()
コンテナを条件によって分割する。分割した結果は配列の配列で返る。
引数を省略すると、各要素の単純な評価結果がtrue, falseのどちらになるかで分割されるが、意味なさげ。
var array = [-4, -3, -2, -1, 0, 1, 2, 3];
var part = array.partition(function(value) {return value >= 0;})
alert(part[0].inject("true", function(memo, value) {return memo + ", " + value;}));
alert(part[1].inject("false", function(memo, value) {return memo + ", " + value;}));
pluck()
指定したプロパティの値を抽出する。
JavaScriptでは全てのものがハッシュなので、この関数が成り立つ。
var array = ["1234", "abc", "ABCDEF"];
var lengthArray = array.pluck("length");
lengthArray.each(alert);
reject()
findAll()の逆で、条件に合わない要素の集合を集める。
var foo = ["a", "bc", "def", "ghij"];
var result = foo.reject(function(value) {return value.length < 3;});
なら["def", "ghij"]が返る。
sortBy()
引数に指定した関数によってソートを行う。
結果を出すまでにハッシュと配列を一回ずつ生成するので、効率は悪そう。
例えば、
var array = ["1234", "abc", "ABCDEF"];
array.sortBy(function(valeu) {return value.length;}).each(alert);
とすると、文字列の長さでソートされる。
toArray()
配列化。collect()で引数を省略したときと同じ。
zip()
複数のコンテナの縦と横を入れ替える。
例えば、
var array = [1, 2, 3, 4];
var zipped = array.zip([10, 20, 30], [-1, -2, -3, -4, -5]);
zipped.each(function(z, i) {
alert(z.inject("[" + i.toString() + "]", function(memo, value) {return memo + ", " + value;}));
});
とすると、[[1, 10, -1], [2, 20, -2], [3, 30, -3], [4, undefined, -4]]が返る
mapはcollectの別名。最後の引数が関数であれば、その関数を各要素に適用した結果が返る。
処理としては、コンテナの配列を作った後に、各コンテナを配列化し、配列化したを縦に並べて横に並んだものを新たな配列として生成する(pluck(index))。
args.last()は、Arrayをprototype.jsが拡張したArray.last()を呼び出しており、コンテナの最後の要素を返す。
inspect()
コンテナを直列化する。配列の直列化した結果を修飾している。
Object.extend(Enumerable, {
map: Enumerable.collect,
find: Enumerable.detect,
select: Enumerable.findAll,
member: Enumerable.include,
entries: Enumerable.toArray
});
Enumerableの各関数に別名を付けているだけ。
var $A = Array.from = function(iterable) {
if (iterable.toArray) {
return iterable.toArray();
} else {
var results = [];
for (var i = 0; i < iterable.length; i++)
results.push(iterable[i]);
return results;
}
}
配列化の関数を定義している。
元から配列的なインターフェースを持っているものを配列化できる。
とりあえず、$A("abcdef")と["a", "b", "c", "d", "e", "f"]が得られる。
Arrayにfrom関数として配列化を追加しているが、意味が分からない(配列を配列化?)。
Object.extend(Array.prototype, Enumerable);
ArrayにEnumerableのインターフェースと実装を追加する。
Object.extend(Array.prototype, {
_each: function(iterator) {
for (var i = 0; i < this.length; i++)
iterator(this[i]);
},first: function() {
return this[0];
},last: function() {
return this[this.length - 1];
},compact: function() {
return this.select(function(value) {
return value != undefined || value != null;
});
},flatten: function() {
return this.inject([], function(array, value) {
return array.concat(value.constructor == Array ?
value.flatten() : [value]);
});
},without: function() {
var values = $A(arguments);
return this.select(function(value) {
return !values.include(value);
});
},indexOf: function(object) {
for (var i = 0; i < this.length; i++)
if (this[i] == object) return i;
return false;
},reverse: function() {
var result = [];
for (var i = this.length; i > 0; i--)
result.push(this[i-1]);
return result;
},inspect: function() {
return '[' + this.map(Object.inspect).join(', ') + ']';
}
});
組み込みのArrayを拡張している。
Enumerableなコンテナに要求されるインターフェースの実装が主。
_each()
「列挙の仕方」インターフェースを実装している。
配列なので、先頭から順に列挙しているだけ。
first()
コンテナの最初の要素。配列なので、シンプル。
last()
コンテナの最後の要素。STLのend()とは違い、last()が返すのは実オブジェクトの参照そのもの。
内部反復子を実装できたので、STL的な外部反復子(begin()とend())は要らない。
compact()
nullやundefinedな値を配列中から除去する。
select()はEnumerable.findAll()と同じ。
flatten()
配列の要素を全て連結する。
配列に配列が含まれている場合を処理するため、value.constructor == Arrayでvalueの方がArrayかどうかを見て、Arrayだったら再帰的にflatten()を呼び出している。
without()
引数に指定した値を取り除いた配列を得る。
[1, 2, 3, 4, 5].without(1, 3, 5)の結果は[2, 4]になる。
indexOf()
配列中をから引数に指定した値を検索し、そのインデックスを得る。
reverse()
配列の反転。自分自身を書き換えるのではなく、反転した新しい配列を得る。
inspect()
配列を直列化する。要素を直列化した結果を", "区切りで得る。JSON?
var Hash = {
_each: function(iterator) {
for (key in this) {
var value = this[key];
if (typeof value == 'function') continue;var pair = [key, value];
pair.key = key;
pair.value = value;
iterator(pair);
}
},keys: function() {
return this.pluck('key');
},values: function() {
return this.pluck('value');
},merge: function(hash) {
return $H(hash).inject($H(this), function(mergedHash, pair) {
mergedHash[pair.key] = pair.value;
return mergedHash;
});
},toQueryString: function() {
return this.map(function(pair) {
return pair.map(encodeURIComponent).join('=');
}).join('&');
},inspect: function() {
return '#<Hash:{' + this.map(function(pair) {
return pair.map(Object.inspect).join(': ');
}).join(', ') + '}>';
}
}
ハッシュとしてのインターフェースを定義している。
JavaScriptではなんでもハッシュなので、これ単体では使い道がない。$H()で暗黙に使われる。
_each()
「列挙の仕方」インターフェースを実装している。
for (key in this) はコンテナが持っている全てのキーを列挙する(for (var key in this)が正しいような気がする)。
値の方が関数だった場合は、処理しない。
値が普通のオブジェクトだった場合は、pairという名前で新たなオブジェクトを生成し、配列としてもハッシュとしてアクセスできるよう、オブジェクトを整えてiterator()に渡す。
keys()
ハッシュの持っているキーを全て配列に入れて返す。perlのkeysと同じ。
_each()が"key", "value"というキーでハッシュを生成してpluck()に渡していることに強く依存しているコードなので、あまりいい感じはしない。
var result = [];
for (var key in this) {
result.push(key);
}
return result;
でいいと思う。
values()
ハッシュの持っている値を全て配列に入れて返す。
keys()と同じく、
var result = [];
for (var key in this) {
result.push(this[key]);
}
return result;
でいいと思う。
merge()
二つのハッシュを連結する。
$H()は普通のハッシュをHashインターフェースを持ったEnumerableなオブジェクトに変換する。
変換した上で渡されたハッシュのメンバーをthisに全て追加している。
同じキーが存在する場合は、渡されたハッシュの値で上書きする。
toQueryString()
ハッシュをGETやPOSTに渡せる形に変換する。
pair.map(encodeURIComponent).join('=')は、ハッシュのkey-valueペアをURL-encodeして'='で連結している。
this.map(function(pair) {~}).join('&')は、その結果を更に'&'で連結している。
inspect()
ハッシュを直列化する。これもJSON風の結果を返す。
function $H(object) {
var hash = Object.extend({}, object || {});
Object.extend(hash, Enumerable);
Object.extend(hash, Hash);
return hash;
}
普通のハッシュをHashインターフェースを持ったEnumerableなオブジェクトに変換する。
元になるハッシュを渡さなかった場合は、空のHashができる。
var Range = Class.create();
Object.extend(Range.prototype, Enumerable);
Object.extend(Range.prototype, {
initialize: function(start, end, exclusive) {
this.start = start;
this.end = end;
this.exclusive = exclusive;
},_each: function(iterator) {
var value = this.start;
do {
iterator(value);
value = value.succ();
} while (this.include(value));
},include: function(value) {
if (value < this.start)
return false;
if (this.exclusive)
return value < this.end;
return value <= this.end;
}
});
値の範囲を意味するオブジェクト。
第三引数は、境界値を含むかどうかのフラグで、trueの場合は終端値を含んだ値が生成される。
new Range(0, 10); // 0~9の整数
new Range(1, 10, true); // 1~10の整数
_each()
「列挙の仕方」インターフェースを実装している。
範囲内の数値を1刻みで列挙し、処理する。
列挙した値が範囲内に含まれない場合は、列挙を停止する。
include()
第一引数で指定する値が範囲内に含まれるかどうかを判定する。
判定がちょっとややこしいのは、終端値を含めるかどうかの判定があるから。
var $R = function(start, end, exclusive) {
return new Range(start, end, exclusive);
}
Rangeオブジェクトを生成するためのsyntax sugar(的なもの)。
使い方はこんな感じ。
$R(0, 10); // 0~9の整数
$R(1, 10, true); // 1~10の整数
- Category(s)
- JavaScript
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/anaka/prototype-js30928aad3093307f308b/tbping
Google Mapsで遊んでみた3