数値をカンマ区切りにする ― 2007年12月07日 00時10分
JavaScript で数値を 3 桁ごとにカンマで区切るお話。「comma separation by javascript - さらさら宇宙忍法帖」にいろいろまとまっている。
ここでの基本的な方針としては 1 回の文字列置換で終わらせること。元のコードは Perlメモの「数字を 3桁ごとにコンマで区切る」。でも小数への対応を考えると 1 回では無理っぽかったので、小数点が含まれる場合は文字列を分割して処理することに。というわけでいきなり結論。
Number.prototype.toDeliminated = function () {
var string = "" + +this;
var pointIndex = string.indexOf(".");
return (pointIndex == -1)
? string.replace(/(\d{1,3})(?=(?:\d\d\d)+$)/g, "$1,")
: string.substring(0, pointIndex)
.replace(/(\d{1,3})(?=(?:\d\d\d)+$)/g, "$1,") +
string.substring(pointIndex)
.replace(/(\d\d\d)(?=\d)/g, "$1,");
};
(-1234.56789).toDeliminated(); // => -1,234.567,89
さらさら宇宙忍法帖の実験コードで試した限りでは、整数に関しては「iandeth. - javascriptで数値をカンマ区切り文字列に変換する関数メモ」よりほんの少し速く、小数に関してはそれよりだいぶ遅いみたい。小数部にカンマを入れる処理をなくせば差はかなり迫ってくるけど。
「Number.prototype.toCommaed2 - ’ellaneous」より。確かに小数点が入ってると仮定すれば 1 回の置換でいけますね。
Number.prototype.toDeliminated = function () {
var string = "" + +this;
return string.replace((string.indexOf(".") == -1)
? /(\d{1,3}(?=(?:\d\d\d)+$))/g
: /(\d{1,3}(?=(?:\d\d\d)+\.)|\d\d\d(?=\d+$))/g,
"$1,");
};
しかし速度はそんなに変わらない。というよりむしろ遅くなってる。失敗時のコストが跳ね上がるからか。
Kanasan.JS #2 レポート & 資料 ― 2007年12月14日 07時51分
関西での JavaScript 勉強会、Kanasan.JS #2 に行ってきました。今回は 9 時から 21 時という長丁場で、内容は前回に引き続き Prototype.js のコードリーディング。ただし、前回はバージョン 1.5.1.1 だったのが今回からは 1.6.0 を使用ということで、コードリーディングに先駆けて 1.6.0 での変更点に関するプレゼンテーションをやらせていただきました。他の参加者の方々のレポートなどは Kanasan さんの記事経由で読めるかと思います。
フリートーク
午前中はフリートーク及びプレゼンテーションということで、自己紹介の後雑多な話題に。
リファレンス
どんな参考文書を利用しているかという話題。とりあえずググって出てきたページという方が結構いるようです。私も検索エンジンを使ったりもしますが、特定のサイトだとこんな感じです。
- コア言語
- DOM / ブラウザオブジェクトモデル
クロージャあれこれ
クロージャが無名である必要はない。
function f(x) {
// 関数宣言によって作られた g もクロージャ
function g() { return x; }
return g;
}
var h = f(42);
h(); // => 42
function nthPowerSum(n) {
// 名前付き関数式によって作られたクロージャ
// ただし Safari 2 以前では構文エラー
return function powerSum(x) {
return (x <= 0) ? 0 : Math.pow(x, n) + powerSum(x - 1);
};
}
var squareSum = nthPowerSum(2);
squareSum(3); // => 14 (== 1 ^ 2 + 2 ^ 2 + 3 ^ 2; ここで ^ は累乗)
関数がクロージャとなりうるのは、その関数が関数宣言または関数式を用いて作られた場合。Function コンストラクタを使うとグローバルで関数宣言したのと同じになる。
var x = "global";
function f(x) {
return function () { return x; };
}
var g = f("argument");
g(); // => argument
function p(x) {
// これは new があってもなくても (コンストラクタ
// 呼び出しでも関数呼び出しでも) 結果は同じ
return new Function("return x;");
}
var q = p("argument");
q(); // => global
クロージャ内でどんな変数が使われるかを静的 (コンパイル時) に決定することはできないので、クロージャは親の環境を基本的にすべて保持する。
function f(x) {
// クロージャ内で x が直接使われていないから
// といって x を切り捨てることはできない
return function () {
return eval("x");
};
}
var g = f(42);
g(); // => 42
前回の Kanasan.JS のまとめ
Kanasan さんによるプレゼンテーション「前回の Kanasan.JS」。Prototype.js はライブラリではなくフレームワークとのこと。コメントにも Prototype JavaScript framework
と書いてある。
Prototype.js 1.5.1.1 と 1.6.0 の違い
コードリーディング第 0 部といった趣でざっとソースに目を通しておこうという流れ。プレゼンテーションには以下を、いずれも Firefox で文字サイズを 3 段階くらい大きくして使いました。余談ですが、ディスプレイの出力設定をいじっていたところ突如スクリーンに映し出される青画面、直後にかかる再起動。Windows XP でのブルースクリーンを久々に見ました。
- Prototype.js 1.6.0 の変更点
- Prototype.js 1.5.1 と 1.6.0 の差分 (実際にはこれから該当箇所だけ抜き出したものを使用)
- よくある、ソースを書いてそれをすぐに実行できますよというもの (Prototype.js 1.6.0 を組み込み済み)
Object.isArray
constructor プロパティを見ているけど、instanceof 演算子を使うこともできる。constructor プロパティは書き換え可能だが、instanceof 演算子はそれに左右されない。
var a = [];
a.constructor = 42;
a.constructor; // => 42
Object.isArray(a); // => false
a instanceof Array; // => true
Function#argumentNames
JScript では Function#toString でコメントを除去しないので、IE で function f(a, /*)*/ b) {}
などとするとおかしなことに。
Function#curry
同様のものに対して Brendan Eich (JavaScript の作成者) いわく、それはカリー化ではなく部分適用だと。
Function#wrap
スライド中の例では以下の二つは同じ。
var 劇的改造 = リフォームする.wrap(紹介する);
var 劇的改造 = function () { 紹介する(リフォームする); };
Function#methodize
apply の第 1 引数に null を指定しているのは、this を第 2 引数の先頭に入れているから? この場合、呼び出された関数内での this はグローバルオブジェクトを指す。
Function#defer
curry の実例。スライドで抜けているのは完全に見落としていたから。本番で気づいてあせった (^^;
Class.create
第 1 引数に Class.create で作られたクラスを指定すると、そのクラスを継承したクラスができる。これは第 1 引数にメソッドを集めたオブジェクトを指定するのとは別物。
マイカー instanceof 車;
// => TypeError: invalid 'instanceof' operand \u8ECA
// instanceof 式の右辺には関数しか取れない
フォクすけ instanceof イヌ科;
// => true
ここで継承の実現のために用いられている手法は基本的に「プログラマのためのJavaScript (号外):こんな継承はどう? - 檜山正幸のキマイラ飼育記」と同じもの。私がこの手法を知ったのはおそらく「JavaScript 継承3 - Starry Night」から (TAKI さんというのは ECMA-262 3rd edition 邦訳をはじめ多数の文書を公開されている TAKI さんですね)。
ちなみに「フォクすけ*ブログ - FAQ」を見てもわかるとおり、フォクすけはキツネです。レッサーパンダではありません。
Class.Methods#addMethods
メソッドの第 1 引数の名前が $super であるときに、親クラスのメソッド呼び出しができるようにするための処理が、無名関数をその場で実行してクロージャを作成、そのクロージャの wrap メソッドを呼び出すと、ぱっと見何をやっているのかさっぱりわからない。あと、これを書いている途中で気づいたけど、valueOf および toString メソッドの上書き処理にバグがある。
var P = Class.create();
var C = Class.create(P, {
x: function ($super) { return "x"; },
y: function ($super) { return "y"; }
});
var o = new C();
o.x.toString();
/* Firefox 2 =>
* function ($super) {
* return "y";
* }
*/
JScript には、あるプロパティに隠された (shadowed) プロパティ (オブジェクトのプロトタイプチェーン上にあるオブジェクトが持つプロパティで、元のプロパティと同じ名前を持つもの) が DontEnum 属性を持つ (for-in 文で列挙されない) とき、元のプロパティまで列挙されなくなるというバグがある。そのままだと独自の toString メソッドなどを定義することができないので、そのような場合には追加するメソッド名に toString と valueOf を加えている。
var a = [];
a.join = 42;
var join = null;
for (var i in a)
if (i == "join")
join = a[i];
join;
// Firefox 2 => 42
// IE 7 => null
コードリーディング
プレゼンテーションは時間が押すものと相場が決まっていますが、今回午前の部は 12 時までの予定が結局 13 時近くまでかかってしまい、13 時 50 分から午後の部、コードリーディングと相成りました。
String#unescapeHTML
IE と Safari に対しては escapeHTML/unescapeHTML の定義を変更しているのだが、それがバグ持ちではないかという話。
"&lt;".unescapeHTML();
// Firefox 2 => <
// IE 7 => <
Template#evaluate
いきなりの難関 Template クラス。基本的な使い方は以下のとおり。String#interpolate 経由でも使える。
new Template("#{foo}").evaluate({ foo: "bar" }); // => bar
"#{foo}".interpolate({ foo: "bar" }); // => bar
"\500" と出力できないという問題が。
"\#{price}".interpolate({ price: 500 }); // => 500
"\\#{price}".interpolate({ price: 500 }); // => #{price}
"\\\#{price}".interpolate({ price: 500 }); // => #{price}
"\\\\#{price}".interpolate({ price: 500 }); // => \#{price}
単位をハードコーディングすべきでないという意見。
"#{unit}#{price}".interpolate({ unit: "\\", price: 500 });
// => \500
そもそも Unicode で円記号は \ ではないという意見。
"\u00a5#{price}".interpolate({ price: 500 });
// => ¥500
プロパティをどんどんたどっていけるようにするため、内部の正規表現が複雑になっている。
"#{a.b.c} #{d[0].e} #{f[g[\\]]}".interpolate({
a: { b: { c: "Hello," } },
d: [{ e: "Template" }],
f: { "g[]": "world!" }
});
// => Hello, Template world!
[^.[]
はドットと開き角括弧の否定文字クラス。JavaScript の正規表現では文字クラス内で開き角括弧をエスケープする必要はない。
Enumerable#each
iterator 内で this が指すオブジェクトを、引数としても指定可能。以下の二つは同じ。
enumerable.each(func.bind(o));
enumerable.each(func, o);
$break が投げられるとそこで反復が止まる。JavaScript では throw 文でどんな値 (null や undefined を含む) でも投げられる。
throw "Windows from window";
var Windows = { out: { of: { the: { window: null } } } };
throw Windows.out.of.the.window;
Enumerable#inject
同様の機能は言語によって reduce、foldl などと呼ばれることも。JavaScript 1.8 からは配列に対して reduce、reduceRight が組み込みで利用可能に。
Enumerable#invoke
pluck + 関数呼び出し。引数も指定可能。
[1, 2, 3].invoke("toString", 2).inspect();
// => ['1', '10', '11']
$A
配列っぽいものを Array オブジェクトに変換。なぜ WebKit だけ動作を変更しようとしているのかは不明。しかもこの書き方だと、IE や Opera でも動作が変更されてしまう (Hash の項を参照)。
Array#first
array[0]
のほうが短いじゃないかという声に対して、まず array.last()
がほしくて (確かに array[array.length - 1]
は長ったらしい)、そうすると対称性から array.first()
もほしくなるとの意見。
Array#reduce
何のためにあるのかよくわからないメソッド。JavaScript 1.8 の Array#reduce (Prototype.js の Enumerable#inject 相当) とも互換性がなく、速やかに非推奨とすべきだという声も。
$w
$w("abc def ghi").inspect();
// => ['abc', 'def', 'ghi']
Array#concat
Opera のときだけ組み込みのメソッドを上書きしている。詳しい理由は不明だが、Opera の Arguments オブジェクトのプロトタイプチェーンには Array.prototype が含まれていることと関係があるようだ。
function f() {
return [].concat(arguments).length;
}
f(1, 2, 3);
// Opera 9.2、Prototype.js なし => 1
// Opera 9.2、Prototype.js あり => 3
Hash
Safari 2 以前にはプロトタイプチェーン上のオブジェクトが持つ隠されたプロパティまで列挙してしまうバグがあり、その対策をしている。
var o = { x: 42, __proto__: { x: 12 } };
for (var i in o)
print(i + ": " + o[i]);
/* Firefox 2、Safari 3 =>
* x: 42
* Safari 2 =>
* x: 42
* x: 42
*/
if 文の中に関数宣言が書かれているように見えるが、これは ECMAScript 3 では文法違反。IE と Opera ではそれが普通の関数宣言であるかのように扱われる。JavaScript 1.5 以降では関数式文 (function expression statement) という独自拡張構文として扱われる。Safari もこれに準じているようだが、微妙にスコープへの登録箇所が異なる。
if (true) {
function f() { return true; }
} else {
function f() { return false; }
}
f();
// Firefox 2、Safari 3 => true
// IE 7、Opera 9.2 => false
{
f();
function f() { print("f is called."); }
}
// Firefox 2 => ReferenceError: f is not defined
// Safari 3 => f is called.
f();
{
function f() { print("f is called"); }
}
// Firefox 2 => ReferenceError: f is not defined
// Safari 3 => ReferenceError: Can't find variable: f
しかし、初期化処理を適切に行っていれば隠されたプロパティが問題になることはないということで、Prototype.js 1.6.0.1 ではこれらの処理がばっさり削られた。
Hash#_each
各要素を配列としても連想配列としても扱えるようにしている。キー名は pair[0]
でも pair.key
でも取れ、値は pair[1]
でも pair.value
でも取れる。
ECMAScript 3 および JavaScript では、for-in 文がプロパティ名をどういう順で列挙するか決められていないので、Hash の順番に依存したコードを書くのは危険かも。ちなみに SpiderMonkey ではプロパティが最初に設定された順で列挙する。ECMAScript 4 でもプロパティが最初に設定された順で列挙するようになる予定。
Hash#toObject
オブジェクトを (浅いコピーではあるが) コピーしているのはどうしてか。変換先のオブジェクトに変更を加えたとき、変換元のオブジェクトまで変わってしまったら、直感的にいやだろうということ。
Hash#index
値を元に対応するキー名を返す。Ruby 1.9 では Hash#index は非推奨で Hash#key を使えとのことだが、Prototype.js では index しか定義されていない。
ObjectRange
名前が Range でないのは DOM 2 Traversal and Range の Range インターフェースとかぶるから。最初は Range という名前だったが、そのことを指摘され 1.4.0 RC3 から ObjectRange に名称変更。
Ajax
組み込みオブジェクトの拡張などが終わり、いよいよブラウザオブジェクトの操作へということで歓声。
Ajax.Responders
個々の Ajax オブジェクトにイベントハンドラ関数を設定しなくとも、一括してすべての Ajax オブジェクトのイベントを受け取れるようにするもの。例えば click イベントを受け取るとき、document にイベントリスナを設定すれば、個々の要素にイベントリスナを設定しなくとも、バブリングしてきた click イベントを受け取れるというのに少し似てるか。
Ajax.Responders.dispatch
イベントハンドラ関数を呼び出す処理。callback には "onCreate" といったイベント名を表す文字列が入る。responder に入ってくるのは onCreate といったイベントハンドラ関数を持つオブジェクト。
以下の 1 行目のようにあるのは 2 行目と同じはずなのに、なぜわざわざ 1 行目のような書き方をしているかは不明。
responder[callback].apply(responder, [request, transport, json]);
responder[callback](request, transport, json);
Ajax.Request#request
HTTP メソッド名を小文字に統一したと思ったら大文字にしたり。それなら最初から大文字に統一しておけばいいのにと思わなくもない。
KHTML/WebKit に対して送信するデータに何か付け足す処理。こういうところにこそコメントがほしいのにない。
Firefox にて同期モードの XMLHttpRequest の readystate イベントが発生しない問題への対処。ここにはきちんとコメントがついている。しかしこれ、昔のバグだと思っていたら現役で、Firefox 3 でも直らない可能性あり?
Ajax なのに同期とはこれいかにという声。Sjax というやつか。x も XML ではなく XMLHttpRequest といったところ。
Ajax.Request#setRequestHeaders
HTTP 要求ヘッダに Prototype.js のバージョンを入れている。
一時変数を用いずにバージョンチェック。String#match で返ってくる配列の要素は文字列だが、数値と比較する際には自動的に数値へ変換される。
(navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005
Ajax.Request#respondToReadyState
JavaScript の MIME タイプをチェック。RFC 4329 Scripting Media Types で規定された applicatio/javascript、application/ecmascript、text/javascript、text/ecmascript (ただし text/javascript、text/ecmascript は非推奨) および慣用的に使われてきた application/x-javascript。ちなみに、規定されているパラメータは charset だけだが、application/ecmascript に対しては version パラメータを無視してはいけないとされている。
IE のメモリリーク対策として、読み込み完了後は XMLHttpRequest オブジェクトの onreadystatechange プロパティに Prototype.emptyFunction を設定している。IE では、ネイティブオブジェクト (ECMAScript で定められた組み込みオブジェクトや Class.create で作成したクラスのオブジェクトなど) だけで閉じていれば循環参照していようと問題ないのだが、間に COM オブジェクト (DOM ノードオブジェクトや ActiveX オブジェクトなど) が入った循環参照があるとメモリリークしてしまう。
IE 6 SP2 の特定版以降および IE 7 では部分的な修正がなされているが、これは文書ツリーに属する DOM ノードオブジェクトおよび IE 7 組み込みの XMLHttpRequest オブジェクトなどに関してのみのようで、文書ツリーに属さない DOM ノードオブジェクトや、ActiveXObject コンストラクタを使って作られたそもそも文書ツリーに属することのできないオブジェクトなどについては、依然としてメモリリーク問題が残っているようだ。
なお、Firefox 1.5 (Gecko 1.8) 以前にも同様の問題があったが、Firefox 2 (Gecko 1.8.1) ではイベントハンドラの保持に弱参照を用いることなどにより、Firefox 3 (Gecko 1.9) ではガベージコレクションにサイクルコレクタを用いることにより解決済み。サイクルコレクタに関しては、内容がやや古いが「A Cycle Collector on Gecko - steps to phantasien t」にも解説がある。
Ajax.Request.Events
XMLHttpRequest オブジェクトの readyState プロパティの値である整数値に対応する名前。
雑感
実は開催の 3 日前に SVN 上では Prototype.js のバージョンが 1.6.0.1 になっていたということでしたが、さすがにそれにはついていけないということで 1.6.0 のまま続行。しかし、1.6.0 と 1.6.0.1 の diff を見る限りでは今まで読んだ分に大きな変更はなく、むしろそれ以降の箇所にバグ修正がいくつか入っているようなので、次回までに 1.6.0.1 が Prototype.js の Web サイト上でも公開されるのなら、1.6.0.1 に切り替えたほうがいいかもしれません。
Lingr によるチャットでは会場にいない人も参加できるのに加え、参考となる Web ページの URI や、サンプルコードとその出力結果を提示するのにちょうどいいですね。自分が持っていない環境での実行結果を確認してほしいというときに、口頭でソースコードを伝えるなんていうのはちょっと無理がありますから。
主催の Kansan さん、氏久さん始め、ネットワーク環境や LAN ケーブルを提供してくださった方々、参加した皆さん、本当にお疲れ様 & ありがとうございました。
最近のコメント