2007-11-03
■ [event][javascript] Kanasan.JS (別名:prototype.jsのソースにツッコミを入れるオフ) に参加してきました
ちょっとしたイベントのはずが20人オーバーの中規模イベントになってしまった(笑) Kanasan.JS。 結局、途中から初心者/上級者の2グループに分かれてコードリーディングを進めることになりました (※初心者グループの方が内容のレベルが高かったという噂もあるけど…!?)。
感想ですが、prototype.jsのソース読みがこんなに面白いとは思わなかった! 他の言語ではありえないJavascriptならではの実装があったりして、とても良い企画だったと思います。 今回はまだ400行(全体の11%)しか進んでいないので、次回以降も続きができるといいなぁ。
とりあえず、大量の印刷物の用意など、いろいろな作業をしてくださった主催kanasanに感謝を。 イベントを行うきっかけを作った@ujihisaもGJですw
というわけで、コードリーディング中に出た話題についてまとめておきますね。
- ソースは現時点での最新の安定版である1.5.1.1のものを使用しました
- 遅刻したので、100行目以前は誰か補完してくれると嬉しいな
- (11/4追記:はこべさんによる補完が!ガラムいいよガラム)
l.31
K: function(x) { return x }
KってなんのKなんでしょうね…? (Kコンビネータでは?という話も出たけど、どう見てもKコンビネータではなくIコンビネータだし)
ちなみに使用例としては、allやanyのデフォルト引数として使用されているようです(今調べた)。
コンビネータというのは自由変数を含まない(=引数を使うだけ)の関数のことです。 コンビネータにもっと親しみたい方は、Unlambdaという言語を学んでみると良いと思いますw
l.102
Function.prototype.bind = function() { var __method = this, args = $A(arguments), object = args.shift(); return function() { return __method.apply(object, args.concat($A(arguments))); } }
「イベントクラスにコールバック関数を登録する」という処理は javascriptでよくありますが、その際にthisを固定するための 関数のようです(bindを使わないと、thisが元のオブジェクトではなくイベントクラスの方を指してしまう)。
これは実際に使ってるところを見ないとよくわからんなー。
l.112
bindAsEventListener=bindのイベントリスナに特化したバージョン。 bindと同じくthisを固定するほか、実行されるときに第1引数が イベントオブジェクトになります。
return __method.apply(object, [event || window.event].concat(args));
イベントオブジェクトの取り方がIEとFireFoxで違うので、 (前者はwindow.event, 後者は関数の引数) その違いをラップしてくれるようです。
l.126
$R(0, this, true).each(iterator);
$RはRangeオブジェクトを作る関数。RangeはRubyistにはおなじみのクラスですね。
l.149
var Try = { these: function() {
Try.these(f1, f2, f3); とやると、関数f1, f2, f3を順に実行して、 例外を返さなかったものの実行結果が返って来ます。 prototype.jsの中では、Ajaxのブラウザごとの違いを吸収するために 使われているようです。
//l.932 var Ajax = { getTransport: function() { return Try.these( function() {return new XMLHttpRequest()}, function() {return new ActiveXObject('Msxml2.XMLHTTP')}, function() {return new ActiveXObject('Microsoft.XMLHTTP')} ) || false; },
これを綺麗に書きたいがためにTry.theseは用意されたのではないか?という説。
l.167
var PeriodicalExecuter = Class.create();
指定した処理を一定時間ごとに実行してくれるクラス。 setIntervalを自分で使っても良いのですが、タイマを止めるのが 面倒なので用意されているようです。(タイマIDがラップされている)
あと、currentlyExecutingというフラグを使って重複起動を防いでくれるとか。 1秒ごとに起動する関数の実行に2秒かかってたら、2回目は起動しない、ということですね (ってそもそも、一つの関数で何秒もかかるとブラウザが固まって良くないのですが…)。
その他出た話題:
- Q.setIntervalに指定する時間ってどれくらいにすればいい?
- A.出来る限り長くする (負荷軽減のため)
- A.「イベントが起こる間隔の半分くらい」
l.213
Stringクラスのgsubメソッド。javascriptにはもともと replaceというメソッドがあるのですが、 Rubyみたいに #{} を使いたいがために gsubが定義されているようです。
使用例(l.343):
return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();
javascriptでは文字列がimmutable (内容を書き換えることができない)なので、文字列を切って貼って結果を作っています。
(余談ですが、この行の最初のgsubだけ「,」のあとにスペースが入ってることにツッコミが入ってました。)
l.239
Stringクラスのscanメソッド。Ruby由来の命名のはずなのに、 RubyのString#scanと返り値が違うのですが…。
l.244
truncate: function(length, truncation) { length = length || 30; truncation = truncation === undefined ? '...' : truncation; return this.length > length ? this.slice(0, length - truncation.length) + truncation : this; },
文字列を指定した長さに切り詰めるメソッド。 lengthのデフォルトが30ってのがなんとも凄いです(笑)。 実用性重視ですね。
次の行では、truncation引数を省略したら '...' という文字列を使うようになっています(※javascriptでは引数の個数が違ってもエラーにならない)。=== は == の厳密なバージョンで、例えば
null == undefined #=> true null === undefined #=> false
のようになります。
他にも null == 0 がtrueになったり (←嘘でしたごめんなさい・11/6追記)、javascriptの == はいろいろと
(他言語経験者には)予想しがたい挙動をするので要注意です。
ここで == と === の違いを一言で説明できれば良いのですが、 仕様書を見て諦めました(^^; ===は型もチェックする (==は暗黙の型変換を許す) という説明で、合ってるかな。
ちなみに、 null + 1 が 1になるとか、true + 1 が2になるとか、 false - 1が -1になるなんて機能もあります (Perl使いのはこべさん曰く:「まあ、良くある事ですよね」)。 Number(undefined) は NaNになります(さすがに)。
l.255
stripTags: function() { return this.replace(/<\/?[^>]+>/gi, ''); },
文字列からタグを取り除く関数、らしいのですが…、 これ本当に安全なんですかね?id:Hamachiya2さんあたりに検証して欲しいものです。
その後にstripScripts, extractScriptsというメソッドもありますが、 そこで使われている正規表現(l.27)も実に怪しいw (</script >と空白を入れるだけでマッチしなくなります)。
ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',
まあ、「自分で書いたscriptタグを取り除く」ものだと 割り切って使えということでしょうか。
そこの正規表現を作っているところに「'img'」みたいな文字列がありますが、
var matchAll = new RegExp(Prototype.ScriptFragment, 'img'); var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
これは <img>タグのことではなく、正規表現のiオプションと mオプションとgオプションのことのようです(ignore case, multi line, global)。 なら 'gmi'とか 'gim'とか、まぎらわしくない順番に並べればいいのに…(笑)。
l.275
escapeHTML: function() { var self = arguments.callee; self.text.data = this; return self.div.innerHTML; },
個人的に今回のハイライトだと思うのがこのescapeHTML関数です。 ご存じ「>」とか「&」を「>」「&」等にエスケープする関数なのですが、javascript以外ではあり得ない実装になっています。
まず、l.419以下で escapeHTML関数にdivとtextというプロパティを定義しています。関数すらオブジェクトとして扱うjavascriptならではですね。
Object.extend(String.prototype.escapeHTML, { div: document.createElement('div'), text: document.createTextNode('') });
そして次に、div.appendChild(text) で divノードにtextノードを子として追加しています。
with (String.prototype.escapeHTML) div.appendChild(text);
これは普通に書くと
String.prototype.escapeHTML.div.appendChild(String.prototype.escapeHTML.text);
のようになるのですが、あまりに長いのでwith構文を使っているようです。
escapeHTML()が呼ばれた時は、まずarguments.calleeでescapeHTML関数自身を参照しています(arguments.callerが呼び出し元の関数、arguments.calleeが呼び出される関数を指す)。 そして、そのテキストノードのdataプロパティにthis(=escapeしたい文字列)を代入し、divノードのinnerHTMLを参照すると エスケープされた文字列が受け取れるという…。
escapeHTML()を日常的に使っている人も、まさかこんな実装になっているとは知らなかったんじゃないでしょうか(笑)。
divノードやテキストノードを関数のプロパティに保存するのは、 実行速度を上げるためのようです(createElementは結構重い)。 また、innerHTMLでエスケープされた文字列が取れるのはDOM 1 で規定されていてブラウザ互換らしい。
l.281
unescapeHTML: function() { var div = document.createElement('div'); div.innerHTML = this.stripTags(); return div.childNodes[0] ? (div.childNodes.length > 1 ? $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) : div.childNodes[0].nodeValue) : ''; },
続いてその逆のunescapeHTML関数です。 こいつはescapeされたタグを復活させるのですが、この3項演算子の 嵐、解読できるでしょうか?
if文に直すと以下のようになります。
if(div.childNodes[0]) { if(div.childNodes.length > 1) return $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) else return div.childNodes[0].nodeValue } else{ return ''; }
要するに、injectで子ノードの文字列表現を集めているのですが、 要素が0個とか1個の時を場合分けすることで、$Aやinjectによる オーバーヘッドを回避しているようです。
と分かればさっきのreturn文もなんとか読めるのですが、 コメントを入れるかインデントを工夫してほしいものです。(^^;
l.289
"http://.../?hoge=1&moge=2#footer" のようなクエリ文字列から、 {"hoge": 1, "moge": 2} のようなハッシュを作るメソッド。 最初の正規表現で「?」と「#」の間の文字列を取り出しています。
var match = this.strip().match(/([^?#]*)(#.*)?$/);
括弧が2つありますが、欲しいデータは1つ目(match[1])だけなので、
var match = this.strip().match(/([^?#]*)(?:#.*)?$/);
と書いても動作は変わりません。
この関数は引数を一つとり、セパレータを変えられるようになっています(デフォルトは'&')。'&' の他には ';' が代表的でしょうか。 ちなみに、逆の動作を行うメソッド toQueryString ではなぜか「'&'」に固定されています(^^;
decodeURIComponentはURI表現(%20とか)をデコードする標準関数です。 他にdecodeURIという関数もあって、挙動が違うようです。
- Core JavaScript 1.5 Reference:Global Functions:encodeURI - MDC
- Core JavaScript 1.5 Reference:Global Functions:encodeURIComponent - MDC
URIにパラメータを埋め込む時にはencodeURIComponentの方を使えば良いようですね。(※encodeURIの方はいつ使うんだろう?)
toQueryParamsという関数名は(Rails使い以外にとって?)直感的じゃないね、という指摘がありましたが、今見たらparseQueryという別名が定義されていました(l.417)。
String.prototype.parseQuery = String.prototype.toQueryParams;
l.313
文字列のsuccメソッド。Rubyのものと挙動が結構違うようですが、 まぁ文字列のsuccとか普通使わないし、どういう挙動が望ましいのか良くわからんです。
l.318
文字列のtimesメソッド。指定した回数だけ繰り返した文字列を作ります。…ここはinjectを使ってないのはなんでなんでしょうね?
l.342
文字列をCamelCaseにしたり、「_」区切りにしたり、「-」区切りにするメソッド(camelize, underscore, dasherize)が定義されています。 underscoreだけ「-ize」でないのは何故?と思ったら、underscoreという他動詞が存在する模様。
ちなみにunderscoreはCamelCaseをほぐすだけでなく、「::」を取り除いたりもします。いかにもRails用の機能ですね。
l.359
StringのtoJSONは、inspectと同じなんですね。
l.369
Ajaxの応答で内部的に使われているメソッド、isJSONおよびevalJSON。 evalに食わすまえに、「明らかにJSONでない」ものは弾いているのですが、その正規表現がこちら。
return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);
真ん中あたりのアルファベットが気になりますが、
- true
- false
- null
- E (数値表現の 1.35E4 等で使う)
を全部合わせたもののようです。え、「s」が入ってないって? 角括弧の中なので、「r-u」が「rstu」と同じ意味になるのでした。
l.384
ある文字列がprefix/suffixになっているかを調べる関数、startsWithとendsWith。
Rubyにも欲しいなぁと言ってたら、1.9では導入されるようです。
でもends_with?, starts_with?ではないんだなぁ。 Python由来なのか。
絶望した!長すぎるレポートに絶望した!
JavaScript中級者(=俺)向けに解説を加えながら書くと、どうしても長くなってしまうなぁ。 ブクマ数は増えるが実際に読む人は減るという諸刃の剣メソッド。
[あとで読む][あとで読まない][あとで読んで]
お疲れ様でした。<br>Firebug 等で確認したところ、0 == null => false だと思います。
> 0 == null => false だと思います<br>やってしまったorz<br>修正しました。ご指摘に感謝!