名前つきの関数リテラルに関するよくわからない IE の挙動

昨日書いた 「IE でも event.currentTarget を使えるようにする」 の記事の後半部分のコードなのですが、IE8 でじっくりと動作を追ってみるとどうも期待通りの動作ではないことがわかりました。

    window.attachEvent("onunload", (function () {
// target._vividcode_el も onunload イベントで解体するので,
// 下手すると参照前に解体されている可能性もある.
// よって, あらかじめ局所変数に読み込んでおく.
var func = target._vividcode_el[i][1];
return function myself(evt) {
target.detachEvent("on"+type, func);
window.detachEvent("onunload", myself);
};
})() );
が問題の箇所で、どうも window.detachEvent("onunload", myself); の部分でちゃんと detachEvent できていないようなのでした。 unload イベントに対応させていたため発覚しづらかったのですが、よくよく調べてみると確かにちゃんと detachEvent できていません。 あれ? なんで?

ということで色々調べてみました。

で、結果としては IE における名前つき関数リテラルFirefox などとは違う挙動をする、ということがわかりました。

言葉の定義

とりあえず関数リテラルって何? というところを定義しておきます。 なんかネット上だと色んな情報があるんで。 ここは恐ろしいインターネッツですね。

私が言う関数リテラルとは

var func = function(args) {
....
};
のように、式の一部として文の中で作られる関数のこと。 あくまで関数部分 (function() { ... }) が関数リテラルです! 関数式とも言います。

マイコミジャーナルさんの記事 だと、上の例の変数 func を 「関数リテラル」、function() { ... } を 「無名関数」 などとみなしているようですが、それはちょっと違うんでないかい? と思うわけで・・・。 まあ今回の記事の争点は 「関数リテラルと無名関数の違い」 とかではないんで、うちの記事では上の定義でやりますよー、と軽く流しましょう。

名前つきの関数リテラル

なぜ関数リテラルが 「無名関数」 と呼ばれるのか、というところですが、通常関数リテラルには関数名を付けないからです。 上の例でも、変数 func というのはあくまで関数リテラルを代入しているだけであって、関数リテラル自体には名前がついていません。

ただ、関数リテラルに名前を付けることができないのかというとそうではなく、名前を付けることもできます。

var func = function myname(args) {
....
// 関数内で、自分自身を指すために myname を使うことが出来る
};
// 関数外では myname という関数は見えない
上のように名前を付けることもできます。 ただし、この名前は関数内でしか利用できません。

名前つきの関数リテラルに関する IE の挙動

で、ここからが本題なのですが、名前付き関数リテラルに関する IE の挙動に関して。

var test = function myname(f) {
window.alert(f === myname ? "一緒" : "違う");
};
test(test);

上のコードを実行すると、どういう結果になるでしょう? 私は "一緒" という文字がポップアップされると思っていました。 しかし、IE8 で実際に実行すると、なぜか "違う" という文字がポップアップされてしまうのです。 謎だ・・・。 IE はホントに斜め上過ぎて困りますね!

ちなみに Firefox 3.5 だとちゃんと "一緒" とポップアップしてくれました。

さらに色々調べると、関数外では myname という名前で関数を参照できないはずなのに、なぜか IE ではできてしまいました!

そんなわけで IE で名前付き関数リテラルを使うときは注意しましょう!

おまけ

var test = function myname(f) {
window.alert(f === myname ? "一緒" : "違う");
};
test(test); // => IE8: "違う", Firefox 3.5: "一緒"
// Firefox の場合下はエラーになる
try {
test(myself); // => IE8: "一緒", Firefox 3.5: Error (myself が未定義のため)
} catch(e) { window.alert(e); }
try {
myself(test); // => IE8: "違う", Firefox 3.5: Error (myself が未定義のため)
} catch(e) { window.alert(e); }

昨日のコードのだめなところ

    window.attachEvent("onunload", (function () {
// target._vividcode_el も onunload イベントで解体するので,
// 下手すると参照前に解体されている可能性もある.
// よって, あらかじめ局所変数に読み込んでおく.
var func = target._vividcode_el[i][1];
return function myself(evt) {
target.detachEvent("on"+type, func);
window.detachEvent("onunload", myself);
};
})() );

それで昨日のコードがどうだめなのかというと・・・。 登録するイベントリスナを関数リテラルで宣言し、それに myself という名前を付け、関数実行時に window.detachEvent("onunload", myself) で detach する、というわけですが、関数リテラルの評価結果 (?) と myselfIE では同一のものだとみなされないため、ちゃんと detach できなかった、というわけです。

修正するならこんな感じですね。

    window.attachEvent("onunload", (function () {
// target._vividcode_el も onunload イベントで解体するので,
// 下手すると参照前に解体されている可能性もある.
// よって, あらかじめ局所変数に読み込んでおく.
var func = target._vividcode_el[i][1];
var cleanupListener = function(evt) {
target.detachEvent("on"+type, func);
window.detachEvent("onunload", cleanupListener);
};
return cleanupListener;
})() );

arguments.callee 【2009.07.13 追記】

同じような内容の記事を発見したので参照。

そこに書いてあった方法なのですが、arguments.callee を使えばわざわざ関数リテラルに名前を付ける必要もないんですね! なるほどなるほど。
var f = function() {
// 自分自身
var self = arguments.callee;
// ....
};