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 でのブルースクリーンを久々に見ました。

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 の定義を変更しているのだが、それがバグ持ちではないかという話。

"&amp;lt;".unescapeHTML();
// Firefox 2 => &lt;
// 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 からは配列に対して reducereduceRight が組み込みで利用可能に。

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

Ruby の %wPerl の qw と似たようなもの。

$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 RangeRange インターフェースとかぶるから。最初は 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 ケーブルを提供してくださった方々、参加した皆さん、本当にお疲れ様 & ありがとうございました。

コメント

_ shogo4405 ― 2007年12月14日 23時20分

わかりやすいプレゼン&素晴らしいレポートありがとうございます。
参考になりました。一応、お昼ごはんご一緒させていただいた人です。

回線は途中までしかご提供できずに申し訳なかったですm(__)m

_ nanto_vi ― 2007年12月16日 00時37分

いえいえ、おかげさまでより議論を深めることができました。次回はぜひ最後までご一緒したいと思います。

コメントをどうぞ

※メールアドレスとURLの入力は必須ではありません。 入力されたメールアドレスは記事に反映されず、ブログの管理者のみが参照できます。

※投稿には管理者が設定した質問に答える必要があります。

名前:
メールアドレス:
URL:
次の質問に答えてください:
「ハイパーテキストマークアップ言語」をアルファベット4文字でいうと?

コメント:

トラックバック

このエントリのトラックバックURL: http://nanto.asablo.jp/blog/2007/12/14/2511762/tb