クロージャでメモリリークするパターン


0   名前: think : 2010/05/12(水) 02:12  ID:1kU6nkWR sub-gm
Tag Indexの過去ログを参考にクロージャを使うことによるメモリリークパターンについて勉強しています。
今までは理解しがたいものだったのですが、以下の匿名さん(ID:DwThZqWm)の書き込みを見て理解できた感触がありました。
window.onload = function(){
    var hl = document.getElementById('HIGHLIGHT');
    hl.onmouseover = function(){
       hl.style.background = '#0000ff';
    }
};

JScript 変数 h1 が DOM ノードを参照し、そのプロパティ(onmouseover)を辿ると再び JScript 変数 h1 が現れる。
こういう JScript 世界と DOM 世界をまたいだ循環参照が生じたとき、IE6 はページを移動してもメモリを解放しない(ブラウザを終了するまでメモリを離さない)。
しばしば再起動が必要だった昔とは違い、今はブラウザを起動させっぱなしの方が多いだろうし、更にはタブで何ページも同時に開いているケースも増えているから、制作者としては注意すべきバグだ。
http://www.tagindex.com/kakolog/q4bbs/1201/1434.html

私はメモリリークする根本的原因はDOMノード(要素ノード)のプロパティを定義する部分にあると考えました。
変数 h1は「id='HIGHLIGHT' を持つ要素ノード」を参照している。
変数 h1のonclickプロパティの中で「id='HIGHLIGHT' を持つ要素ノード」を参照している。(hl.style.background)
従って、hl.style.background を参照するとき、「プロパティを参照→要素ノードを参照→プロパティを参照...」のように循環参照してしまう。

ここで考えをまとめるために、下記にあげる5つのケースでメモリリークするかどうか考えてみました。
<p id="Target">Hello, World!</p>
<script type="text/javascript">
var target = document.getElementById('Target');	// <p id="Target"> の要素ノードを参照する変数targetを定義

// -------- メモリリークするパターン
target.onclick = function(){	// ケース1
	target.className = 'foo';	// targetを参照するtargetのonclickプロパティの中でtargetを参照しているため、循環参照してしまう
};

target.onclick = funtion(){	// ケース2
	this.className = 'foo';	// targetのonclickプロパティの中でtargetを参照するthisを使用しているため、循環参照してしまう
};

target.onclick = funtion(evt){	// ケース3
	var targetElement = (evt ? evt.target : window.event.srcElement); // イベント呼び出し元の要素ノード(<p id="Target">)を参照する変数targetElementを定義 (Firefox : IE)
	targetElement.className = 'foo';	// targetのonclickプロパティの中で <p id="Target"> を参照するevent.targetを使用しているため、循環参照してしまう
};

// -------- メモリリークしないパターン
document./*@cc_on @if(@_jscript) attachEvent('on' + @else @*/addEventListener(/*@end @*/ 'click', function (evt) {	// ケース4
	var targetElement = evt.target || evt.srcElement; // イベント呼び出し元の要素ノード (Firefox || IE)
	if(targetElement.id != 'Target'){ return false; }	// 呼び出し元の要素ノードが id="Target" じゃなければ、抜ける
	targetElement.className = 'foo';	// イベントを定義しているdocumentオブジェクトとイベント呼び出し元の要素ノード(<p id="Target">)に関連性がないため、循環参照しない
},false);

// -------- メモリリークするか不明なパターン
target./*@cc_on @if(@_jscript) attachEvent('on' + @else @*/addEventListener(/*@end @*/ 'click', function (evt) {	// ケース5
	var targetElement = evt.target || evt.srcElement; // イベント呼び出し元の要素ノード (Firefox || IE)
	if(targetElement.id != 'Target'){ return false; }	// 呼び出し元の要素ノードが id="Target" じゃなけば、抜ける
	targetElement.className = 'foo';	// イベントを定義しているtargetとイベント呼び出し元の要素ノード(<p id="Target">)は同じ要素ノードを参照しているが、targetのプロパティとして定義しているわけではないので循環参照しない…?(自信なし)
},false);
</script>


質問事項をまとめます。

Q1. この考えは正しいでしょうか?
Q2. 「ケース5」においてはメモリリークしないのでしょうか? (の考え方を教えてください)

onclick の挙動は理解できた気がするのですが、addEventListner,attachEvent を使用したときの挙動に自信がありません。
アドバイスいただければ幸いです。


# 参考リンク

メモリーリークの備忘録 - babu_babu_babooのごみ箱
http://d.hatena.ne.jp/babu_babu_baboo/20100414/1271205292
メモリーリークパターンを理解する - babu_babu_babooのごみ箱
http://d.hatena.ne.jp/babu_babu_baboo/20100417/1271465430

1   名前: think : 2010/05/12(水) 02:12  ID:1kU6nkWR sub-gm
「ケース5」ですが、下記は余計でした。
	if(targetElement.id != 'Target'){ return false; }	// 呼び出し元の要素ノードが id="Target" じゃなけば、抜ける

<p id="Target"> の要素ノードを参照するtargetにイベントを定義しているのですから、targetElement は必ず <p id="Target"> の要素ノードを参照しますね。
失礼しました。

2   名前: ai : 2010/05/12(水) 02:12  ID:iDDBEDAK sub-Ax
玉砕覚悟で発言してみる。

まず、少し気になった事。
ケース5のプログラムについてですが、
p#Target 要素でイベントハンドラを作っても、
p#Target 要素の子孫でクリックすれば、TargetElement にはその子孫が入るものと思われます。
ですから
if(targetElement.id != 'Target'){ return false; }
は必要かと。(もしくは、p要素まで親を辿るなど。)

次にメモリリークについてですが、
循環参照がメモリリークを起こす一因であるという考えについては、私も同意見です。
ケース5でも
target ( p#Target 要素 ) がクロージャへの参照を持ち、( attachEvent でも参照を持ちます(と思う…) )
クロージャは target、つまり p#Target 要素 への参照を持つ。( クロージャのスコープがクロージャの外にもあるため、
クロージャが解放されない限りクロージャのスコープ範囲内になる変数への参照が断ち切れない。(と思う…) )
ということで循環参照が起こっているものだと思います。
( targetElement はあまり関係がないものと思われます。 )

イベントのデタッチを行うか、target = null; をすれば一応は大丈夫かと。
( target = null をすれば、クロージャは target への参照を持っているが、それは p#Target 要素でなくなるので、
循環参照ではなくなる )

参考
http://msdn.microsoft.com/ja-jp/library/bb250448%28VS.85%29.aspx
(元が英語で、和訳が少し変です)

間違ってたら申し訳ないです。

3   名前: think : 2010/05/12(水) 02:12  ID:1kU6nkWR sub-gm
>>2
玉砕覚悟の回答に感謝!
私は「おそらく、合っているだろう」と感じていたものの一気に情報を頭に詰め込んで自信が持てない状態でした。
賛同を得られて心強いです。

> p#Target 要素でイベントハンドラを作っても、
> p#Target 要素の子孫でクリックすれば、TargetElement にはその子孫が入るものと思われます。
おお、確かに!(子孫要素は考慮に入れていませんでした)
targetElementのチェックは必要ですね。
後でまとめる際に、コメントに備考として付け加えたいと思います。(funtionのtypoとか微修正も兼ねてw)

> クロージャは target、つまり p#Target 要素 への参照を持つ。( クロージャのスコープがクロージャの外にもあるため、
> クロージャが解放されない限りクロージャのスコープ範囲内になる変数への参照が断ち切れない。(と思う…) )
> ということで循環参照が起こっているものだと思います。
> ( targetElement はあまり関係がないものと思われます。 )
なるほど、そのように解釈するのですか…。
「クロージャは target、つまり p#Target 要素 への参照を持つ」というところが新しい発見でした。

とすると、この現象はケース5だけでなく、ケース1〜4にも関係しているのでしょうか?
私は「クロージャの中でtargetと同じ要素ノードを参照している処理があると循環参照している」と>>0で解釈していましたが、実際はクロージャの中身は循環参照であると解釈することも出来ます。
ですので、下記にあげる「ケース6」でも循環参照してしまうのではないでしょうか…。
target.onclick = function(){	// ケース6
	// このクロージャはtargetを参照するため、循環参照してしまう
	window.alert('foo');	// この処理は循環参照とは無関係
};

実際、リンク先もよく読んでみると、
element.expandoClick = ClickEventHandler;
function ClickEventHandler(){
	// このクロージャは要素を参照します。
}
http://msdn.microsoft.com/ja-jp/library/bb250448%28VS.85%29.aspx
(一部、見やすいようにインデント整形しています)

と書かれており、クロージャの中身が空にも関わらず、循環参照していることが示唆されています。

4   名前: think : 2010/05/12(水) 02:12  ID:1kU6nkWR sub-gm
>>2
> 参考
> http://msdn.microsoft.com/ja-jp/library/bb250448%28VS.85%29.aspx
> (元が英語で、和訳が少し変です)
実は、この参考URLは>>0にあるbabu_babu_babooさんのブログ記事に付け加えられていて、私も読んでいたところです。
私もaiさんが解釈したように、「ケース5で循環参照が起こる」と読めました。
<script language="JScript">
function AttachEvents(element){
	// この構造体により、要素が ClickEventHandler を参照します。
	element.attachEvent("onclick", ClickEventHandler);
	function ClickEventHandler(){
		// このクロージャは要素を参照します。
	}
}
function SetupLeak(){
	// リークがすべて一度に発生します。
	AttachEvents(document.getElementById("LeakedDiv"));
}
function BreakLeak(){}
</script>
</head\>
<body onload="SetupLeak()" onunload="BreakLeak()">
<div id="LeakedDiv"></div>
http://msdn.microsoft.com/ja-jp/library/bb250448%28VS.85%29.aspx
(一部、見やすいようにインデント整形しています)

形は違えど、>>0のケース5と同じ状況であり、「ケース5でも循環参照が起きる」と解釈できます。
次に解決法を。
<script language="JScript">
function AttachEvents(element){
	// これを削除するために、
	// どこかに置く必要があります。別の参照を作成します。
	element.expandoClick = ClickEventHandler;
	// この構造体により、要素が ClickEventHandler を参照します。
	element.attachEvent("onclick", element.expandoClick);
	function ClickEventHandler(){
		// このクロージャは要素を参照します。
	}
}
function SetupLeak(){
	// リークがすべて一度に発生します。
	AttachEvents(document.getElementById("LeakedDiv"));
}
function BreakLeak(){
	document.getElementById("LeakedDiv").detachEvent("onclick",
	document.getElementById("LeakedDiv").expandoClick);
	document.getElementById("LeakedDiv").expandoClick = null;
}
</script>
</head>
<body onload="SetupLeak()" onunload="BreakLeak()">
<div id="LeakedDiv"></div>
http://msdn.microsoft.com/ja-jp/library/bb250448%28VS.85%29.aspx
(一部、見やすいようにインデント整形しています)

onunload 時に detachEvent(), element.expandoClick = null; して参照を断ち切っているようです。
>>0のケース1なら、target.onclick = null; になるのだと思います。
循環参照は回避できないので、onunload時に参照を断ち切るわけですね。

> イベントのデタッチを行うか、target = null; をすれば一応は大丈夫かと。
この方法は見覚えがあります。ログをあさって発見。

IE6のメモリリークを華麗に回避 - zorioの日記
http://d.hatena.ne.jp/zorio/20080609/1213028969
Ajaxian &#187; Is “finally” the answer to all IE6 memory leak issues?
http://ajaxian.com/archives/is-finally-the-answer-to-all-ie6-memory-leak-issues

onclickではなく、要素ノードを null で埋めて、参照を切り離すのですね。
この場合は、onunload を考えなくていいので楽できますねw

# 理屈はわかるのですが、null で参照を切り離した状態でどうして onclick が有効なのか?
# ちょっと不思議…。

5   名前: think : 2010/05/12(水) 02:12  ID:1kU6nkWR sub-gm
>>3の発言を訂正します。

[訂正前]
私は「クロージャの中でtargetと同じ要素ノードを参照している処理があると循環参照している」と>>0で解釈していましたが、実際はクロージャの中身は循環参照であると解釈することも出来ます。

[訂正後]
私は「クロージャの中でtargetと同じ要素ノードを参照している処理があると循環参照している」と>>0で解釈していましたが、実際はクロージャの中身は循環参照と無関係であると解釈することも出来ます。

6   名前: think : 2010/05/12(水) 02:12  ID:1kU6nkWR sub-gm
>>3で持ち上がった懸案事項をまとめておきます。
<p id="Target">Hello, World!</p>
<script type="text/javascript">
(function(){	// 念のため、匿名関数で括っておく
	var target = document.getElementById('Target');	// <p id="Target"> の要素ノードを参照する変数targetを定義

	/* onclickを使う全てのケース (「ケース1〜3」「ケース6」に符号) */
	target.onclick = function(){
		// このクロージャはtargetを参照するため、中に何が書かれていようと循環参照してしまう
	};

	/* document.attachEvent() を使うケース (ケース4に符号) */
	document./*@cc_on @if(@_jscript) attachEvent('on' + @else @*/addEventListener(/*@end @*/ 'click', function (evt) {
		// このクロージャはdocumentを参照するため、中に何が書かれていようと循環参照してしまう
	},false);

	/* target.attachEvent() を使うケース (ケース5に符号) */
	target./*@cc_on @if(@_jscript) attachEvent('on' + @else @*/addEventListener(/*@end @*/ 'click', function (evt) {
		// このクロージャはtargetを参照するため、中に何が書かれていようと循環参照してしまう
	},false);
})();
</script>

この仮説が正しいとすると、非常に影響力が大きく、メモリリークしない状況はあり得ないんじゃないかと思えるほどです…。
もしも、「windowオブジェクトまで影響する」なら、外部JavaScriptを使用した時点で必ずメモリリークすることになってしまいます。
<script type="text/javascript">
window.onload = function(){
	// このクロージャはwindowを参照するため、中に何が書かれていようと循環参照してしまう
}

window./*@cc_on @if(@_jscript) attachEvent('on' + @else @*/addEventListener(/*@end @*/ 'load', function (evt) {
	// このクロージャはwindowを参照するため、中に何が書かれていようと循環参照してしまう
},false);
</script>

7   名前: think : 2010/05/12(水) 02:12  ID:1kU6nkWR sub-gm
>>6の件に関して、babu_babu_babooさんに質問してみました。
http://d.hatena.ne.jp/babu_babu_baboo/20100417/1271465430

後で気が付きましたが、微妙にマルチポストしていました。
報告が遅れて、申し訳ありませんでした。m(_ _)m

質問内容は「windowやdocumentまでメモリリークの対象となるか?」で回答は以下の通り。
babu_babu_baboo 2010/04/18 07:23
...
nodeを操る命令が、document. にしかくっつかないのだからそこまで行かない。
http://d.hatena.ne.jp/babu_babu_baboo/20100417/1271465430

この回答を私なりに解釈すると、まず、

・DOMプロセッサが管理しているオブジェクト(ノード)は循環参照するが、スクリプトエンジンが管理しているオブジェクトは循環参照しない。
・documentの配下にDOMプロセッサが管理するノードが存在する。


という大前提があり、windowおよびdocumentはノードではないので、循環参照しない、という結論になるのでしょうか。
<script type="text/javascript">
window.onload = function(){
	// このクロージャはwindowを参照するが、windowオブジェクトはスクリプトエンジンが管理しているため、循環参照しない
};

window.onunload = function(){
	// このクロージャはwindowを参照するが、windowオブジェクトはスクリプトエンジンが管理しているため、循環参照しない
};

window./*@cc_on @if(@_jscript) attachEvent('on' + @else @*/addEventListener(/*@end @*/ 'load', function (evt) {
	// このクロージャはwindowを参照するが、windowオブジェクトはスクリプトエンジンが管理しているため、循環参照しない
},false);

document.onclick = function(){
	// このクロージャはdocumentを参照するが、documentオブジェクトはスクリプトエンジンが管理しているため、循環参照しない
};

document./*@cc_on @if(@_jscript) attachEvent('on' + @else @*/addEventListener(/*@end @*/ 'click', function (evt) {
	// このクロージャはdocumentを参照するが、documentオブジェクトはスクリプトエンジンが管理しているため、循環参照しない
},false);
</script>

8   名前: think : 2010/05/12(水) 02:12  ID:1kU6nkWR sub-gm
この問題は完全ではありませんが、理解しました。
< さんの書き込みが大変参考になったので、下記に引用します。
(重要なセンテンスを強調しました。)

< 2010/04/15 08:46
メモリリークパターンはいくつかありますが、最低限避けるべきは以下です。
node.prop = node;

ここで、node は「DOM プロセッサが管理しているオブジェクト」、prop は「勝手に付け足したプロパティ」=「スクリプトエンジンが管理しているオブジェクト」です。つまり、オブジェクトの管理者が違うのです。

オブジェクトの管理者が同じなら、循環参照しても問題ありません。
document.documentElement.parentNode = document;
Object.prototype.constructor = Object;

問題になるのは、循環参照しているオブジェクトの管轄が異なる場合です。

---

いわゆるクロージャが問題になるパターン。
function hoge(node) {
  node.onclick = function(e) {
    // 変数 node を保持
   };
}

node(DOM)→ onclick(DOM)→ Function(script)→ node(DOM) と、管轄が異なるオブジェクトの循環参照が成立しました。

変数 node は、onclick が生きている限り保持されます。ところが、変数 node が保持されている間は onclick を破棄できません。この堂々巡りが、いわゆるメモリリーク問題の根本です。

もし、node がグローバル変数ならページ破棄と同時に捨てられるのですが、ローカル変数であることが話をややこしくします。結局、変数 node または onclick(にフックされた関数)を手動で破棄する必要があります。

---

下記はメモリリークパターンに該当しません。
function hoge() {
  document.getElementById('hoge').onclick = function(e) {
    // 保持すべきローカル変数がない
  };
}

下記も同様です。
function hoge() {
  var nodes = document.getElementsByTagName('A');
  var I = nodes.length;
  var i;
  for (i = 0; i < I; i++) {
     nodes[i].onclick = function() {
      // 変数 nodes、I、i を保持
    };
  }
}

前者はともかく、なぜ後者が大丈夫なのか。NodeList は「生きて」います(nodes[i] が nodes.item(i) の省略形であることを思い出して下さい)。nodes と i が与えられても、前回と同じノードを参照するとは限りません。結果として、変数参照の連鎖が切れています。

---

IE7 は、ページを破棄するときに DOM 木を辿って各ノードのイベントハンドラを断ち切ります。しかし、DOM 木に属さないノードのイベントハンドラには触れません。例えば createElement() + replaceChild() したノード、あるいは innerHTML でごっそり入れ替えられた断片内のイベントハンドラは、メモリリークします。

IE8 ではエンジンが根本的に改善されたらしく、基本的に上記のメモリリークパターンの心配はありません(個別的な別パターンはありますが、触れません)。

Fx2 以前にも同種のメモリリーク問題があります。そもそも DOM プロセッサとスクリプトエンジンが別機構である以上、この種の循環参照問題はある意味、避けられない問題です。事実、WebKit でも同じ問題が指摘されて修正されましたし、今後も何らかの形で噴出する可能性はあります。
http://d.hatena.ne.jp/babu_babu_baboo/20100414/1271205292




この件は、babu_babu_babooさんもまとめられています。

メモリーリークパターンを理解する - babu_babu_babooのごみ箱
http://d.hatena.ne.jp/babu_babu_baboo/20100417/1271465430
メモリーリークの備忘録 - babu_babu_babooのごみ箱
http://d.hatena.ne.jp/babu_babu_baboo/20100414/1271205292



MSDN, Microsoftサポート技術情報にも情報があります。

Internet Explorer リーク パターンを理解して解決する (日本語版。機械翻訳的な文章で若干読みづらいです。)
http://msdn.microsoft.com/ja-jp/library/bb250448
Understanding and Solving Internet Explorer Leak Patterns (英語版。原文を確認をしたい方に。)
http://msdn.microsoft.com/en-us/library/bb250448
HTML ページの DOM オブジェクトへの循環参照はメモリ リークが発生します。
http://support.microsoft.com/kb/830555/ja
Windows XP ベースのコンピュータで JScript スクリプトを使用する Web ページを表示すると、Internet Explorer 6 でメモリ リークが発生する
http://support.microsoft.com/kb/929874/ja/

9   名前: think : 2010/05/12(水) 02:12  ID:1kU6nkWR sub-gm
これだけでは「あまりにも」なので、私が理解した範囲で解説してみます。


■「スクリプトエンジンが管理しているオブジェクト」と「DOM プロセッサが管理しているオブジェクト」
var obj = {};	// スクリプトエンジンが管理しているオブジェクト
obj.foo = obj;	// 循環参照させる

Object, Function, Array, String などのオブジェクトは「スクリプトエンジンが管理しているオブジェクト」です。
「スクリプトエンジン内で管理しているオブジェクト」同士なら、循環参照してもメモリリークしません。

Under Translation of ECMA-262 3rd Edition
http://www2u.biglobe.ne.jp/~oz-07ams/prog/ecma262r3/


document.body = document;	// DOM プロセッサが管理しているオブジェクト(DOMノード) を循環参照させる

window, documentのオブジェクト及び、window配下にあるオブジェクトは「DOMプロセッサ管理しているオブジェクト」です。
(要素ノード、テキストノードなどのDOMノード、onclick,onload などのイベントハンドラ等も該当します。)
「DOMプロセッサ管理しているオブジェクト」同士なら、循環参照してもメモリリークしません。


■クロージャによる循環参照 メモリリークパターン

クロージャは関数の内部で関数が宣言されたときに発生し、外部関数をエンクロージャ、内部関数をクロージャと呼びます。
function hoge() {	// エンクロージャ
	var element = document.getElementById('Test');	// ローカル変数(DOM)
	// 変数element(DOM)への参照は関数呼び出しが完了すると解放される [1]

	element.attachEvent('onclick', function(event){	// クロージャ
		// element(DOM) → onclick(DOM) → Function(script) → element(DOM)
		// 変数element(DOM)への参照を保持する [2]
	});
}

これはメモリリークします。
ローカル変数elementはエンクロージャ(関数hoge) の中で「#Test(DOM) への参照」を持っています。上記コードの [1] の部分です。以降、参照[1] と呼びます。
一方、attachEventの第二引数に割り当てたクロージャの中でも「#Test(DOM) への参照」を持っています。上記コードの [2] の部分です。以降、参照[2] と呼びます。

参照[1] はエンクロージャが呼び出されたときに初期化され、処理完了後に解放されます。これは通常の関数処理と同じです。
参照[2] はクロージャが存在する限り、参照を保持し続けます。これがクロージャの特徴です。

参照[2] を解放するためにはクロージャを解放する必要があります。
クロージャを解放するためには、onclickイベントハンドラを削除する必要があります。
しかし、クロージャが 参照[2] を保持し続けるためにonclickイベントハンドラを削除できない…という堂々巡りで循環参照してしまっています。
従って、メモリを解放できず、メモリリークが発生します。
通常の循環参照では、相互への参照を保持する 2 つのソリッド オブジェクトがありましたが、クロージャは異なります。直接参照する代わりに、親関数のスコープから情報をインポートすることで参照が行われます。通常、関数のローカル変数と、関数を呼び出すときに使用されるパラメータは、関数自体の継続期間中のみ存在します。クロージャを使用すると、これらの変数とパラメータは、クロージャが有効である限り未解決の参照を持ち続けます。また、クロージャは親関数の継続期間後も存在できるため、その関数のローカル変数とパラメータもすべて存在できます。
例では、通常、パラメータ 1 は関数呼び出しが終了するとすぐに解放されます。クロージャを追加したため、第 2 の参照が作成されます。この第 2 の参照は、クロージャが解放されるまで解放されません。
クロージャをイベントにアタッチした場合は、そのイベントからデタッチする必要があります。クロージャを expando にアタッチした場合は、その expando を null にする必要があります。
http://msdn.microsoft.com/ja-jp/library/bb250448

attachEvent
http://msdn.microsoft.com/en-us/library/ms536343%28VS.85%29.aspx


function hoge() {	// エンクロージャ
	var element = document.getElementById('Test');	// ローカル変数(DOM)

	document.getElementById('Target').onclick = function(event){	// クロージャ
		// #Target(DOM) → onclick(DOM) → Function(script) → element(DOM)
		// 変数element(DOM)への参照を保持する
	});
}

同じく、メモリリークします。
イベント呼び出し元のDOMノード(#Target)とローカル変数に保持しているDOMノード(#Test)が同一ではないことが不思議に思えるかもしれませんが、この場合は重要ではありません。
クロージャはエンクロージャで宣言されたローカル変数element(DOM)への参照を保持し続けます。

element.onclick - MDC
https://developer.mozilla.org/en/DOM/element.onclick


■メモリリークの発生条件

メモリリークの発生条件としては、下記4点があげられると思います。

1. エンクロージャとクロージャが存在する
2. エンクロージャでローカル変数を宣言している
3. エンクロージャで宣言された変数が「DOMプロセッサが管理しているオブジェクト」を参照している
4. 「DOMプロセッサが管理しているオブジェクト」がクロージャを参照している

逆にいえば、1つでも条件を満たさなければメモリリークしません。

10   名前: think : 2010/05/12(水) 02:12  ID:1kU6nkWR sub-gm
■クロージャを使うが、メモリリークしないパターン
function hoge() {	// エンクロージャ
	window.element = document.getElementById('Test');	// グローバル変数(DOM)

	element.attachEvent('onclick', function(event){	// クロージャ
		// element(DOM) → onclick(DOM) → Function(script) → element(DOM)
		// 変数element(DOM)への参照を保持する
	});
}

これは循環参照はしますが、メモリリークしません。
グローバル変数である変数elementはページ破棄と同時に捨てられるからです。

Variables - MDC
https://developer.mozilla.org/ja/Core_JavaScript_1.5_Guide:Variables#.e3.82.b0.e3.83.ad.e3.83.bc.e3.83.90.e3.83.ab.e5.a4.89.e6.95.b0


function hoge() {	// エンクロージャ
	document.attachEvent('onclick', function (event){	// クロージャ
		// document(DOM) → onclick(DOM) → Function(script) → document(DOM)
		// 保持すべきローカル変数がない!
	},false);
}

クロージャからdocumentにアクセスすることは可能ですが、documentはグローバルオブジェクトなので、ページ破棄と同時に捨てられます。
同じ理由で window.attachEvent() もメモリリークしません。

# いわゆる「条件付きコンパイルを利用した手法」がメモリリークしないのはこのためです。詳しくは後述します。


function hoge() {	// エンクロージャ
	var element = document.getElementById('Test');	// ローカル変数(DOM)

	element.attachEvent('onclick', function(event){	// クロージャ
		// element(DOM) → onclick(DOM) → Function(script) → element(script)
		// 変数element(script)への参照を保持する
	});
	element = null;	// DOMノードへの参照を断ち切る
}

これは循環参照しませんので、メモリリークしません。
クロージャが参照を保持する変数elementは null で初期化された時点で「スクリプトエンジンが管理するオブジェクト」となります。

不要になったローカル変数(DOM)は null でDOMノードへの参照を切っておきましょう。
MSDNによると、ローカル変数(DOM)を保持し続けたければ、unload時に null で参照を切ることが勧められています。
リーク パターンを解決するために、明示的な null 割り当てを利用できます。
ドキュメントをアンロードする前に null を割り当てることにより、スクリプト エンジンに対して、エンジン内の要素とオブジェクト間の関連がなくなったことを通知します。
ここで、参照を適切にクリーンアップし、DOM 要素を解放できます。
http://msdn.microsoft.com/ja-jp/library/bb250448

11   名前: think : 2010/05/12(水) 02:12  ID:1kU6nkWR sub-gm
■条件付きコンパイルを利用したコード

「条件付きコンパイル」とはIEのJScriptエンジンで使えるコメントを使った独自記法です。
条件付きコンパイルはコメントを利用するため、IE以外のブラウザでは解釈しませんが、IEだけは条件付きコンパイルを解釈できます。
この挙動を利用すると、「IE」と「IE以外のブラウザ」で処理を分岐させることが出来ます。

条件付きコンパイル
http://msdn.microsoft.com/ja-jp/library/ahx1z4fs%28v%3DVS.80%29.aspx
ブラウザの機能の検出 (JScript)
http://msdn.microsoft.com/ja-jp/library/0hyey391%28v%3DVS.80%29.aspx



以下に紹介するのは条件付きコンパイルを利用して、IEでは attachEvent() を利用し、IE以外のモダンブラウザでは addEventListener() を利用するコードです。
(私はこの記法をTAG Indexの過去ログで何度か確認しています。)
window./*@cc_on @if(@_jscript) attachEvent('on' + @else @*/addEventListener(/*@end @*/ 'load', function (event) {
	alert('windowの読み込み完了!');
},false);

これは一般的な書き方です。
@cc_on で条件付きコンパイルの開始を宣言し、@end で終了を宣言します。
@if, @else で処理を分岐させます。ここで使用している @_jscript は常に true を返します。ですので、IEは必ずパスすることになります。
(MSDNには明確に書かれていませんが、おそらく「JScript を使えるか否か」のフラグでしょう。)
@if 以降にIEで使用する attachEvent() を、IE以外のブラウザで解釈させるために @else 以降のコメントを解除して addEventListener() を指定しています。
addEventListener() の 第三引数(false) は引数を2つしか取れない attachEvent() にも渡してしまいますが、より多くの引数を指定しても無視されるので問題はありません。

条件付きコンパイル ステートメント (JScript)
http://msdn.microsoft.com/ja-jp/library/7kx09ct1%28v%3DVS.80%29.aspx
条件付きコンパイル変数
http://msdn.microsoft.com/ja-jp/library/7142yyxw%28v%3DVS.80%29.aspx


window./*@cc_on @if(1) attachEvent('on' + @else @*/addEventListener(/*@end @*/ 'load', function (event) {
	alert('windowの読み込み完了!');
},false);

こちらも時々見かけます。
"1" は真を返すので、IEでは必ずパスすることになります。(上の @_jscript と同じ理屈です)


window./*@cc_on @if(@_jscript_version<=5.8) attachEvent('on' + @else @*/addEventListener(/*@end @*/ 'load', function (event) {
	alert('windowの読み込み完了!');
},false);

こちらは他で見たことはありません。
IE9で addEventListener() が実装される事を知ってから作りました。
今までの例はIEでは attachEvent() を利用するものでしたが、そうするとIE9でも attachEvent() を使ってしまいます。
それは勿体ないので、@_jscript_version が 5.8(IE8のJScriptバージョン) 以下であった場合に attachEvent() を利用するようにしてみました。

Internet Explorer 9 Preview Builds
http://msdn.microsoft.com/en-us/ie/ff468705.aspx#_Document_Object_Model
Internet Explorer 9 Preview と HTML5 &#171; HTML5.JP ブログ
http://www.html5.jp/blog/2010/03/17/ie9-and-html5/
JScript のバージョン情報
http://msdn.microsoft.com/ja-jp/library/2z6exc9e%28v%3DVS.80%29.aspx
JScript - Wikipedia
http://ja.wikipedia.org/wiki/JScript



■条件付きコンパイルによるリークパターン
document./*@cc_on @if(@_jscript_version<=5.8) attachEvent('on' + @else @*/addEventListener(/*@end @*/ 'click', function (event) {
	// 保持すべきローカル変数がない!
},false);

メモリリークしません。
そもそも、このやり方はクロージャを使う必要がありません。
ですので、「グローバルオブジェクトがページ破棄と同時に捨てられる現象」とは関係なしにメモリリークする心配はありません。
ただし、このやり方が安全だと思いこんでしまうとメモリリークを招いてしまうかもしれません。


function hoge() {	// エンクロージャ
	var element = document.getElementById('Test');	// ローカル変数(DOM)

	document./*@cc_on @if(@_jscript_version<=5.8) attachEvent('on' + @else @*/addEventListener(/*@end @*/ 'click', function (event) {	// クロージャ
		// document(DOM) → onclick(DOM) → Function(script) → element(DOM)
		// 変数element(DOM)への参照を保持する
	},false);
}

メモリリークします。
document は大丈夫でも、もう一つのDOMノードを参照しているローカル変数elementを保持し続けるためです。


function hoge() {	// エンクロージャ
	var doc = document;	// ローカル変数(DOM)

	document./*@cc_on @if(@_jscript_version<=5.8) attachEvent('on' + @else @*/addEventListener(/*@end @*/ 'click', function (event) {	// クロージャ
		// document(DOM) → onclick(DOM) → Function(script) → doc(DOM)
		// 変数doc(DOM)への参照を保持する
	},false);
}

メモリリークします。
document はグローバルオブジェクトですが、ローカル変数docに格納した時点で参照を保持し続けます。

12   名前: think : 2010/05/12(水) 02:12  ID:1kU6nkWR sub-gm
■HTMLイベント属性を使って、メモリリークを回避する
<script type="text/javascript">
function hello(event){
	var target = event.target || event.srcElement;	// イベント呼び出し元ノード (モダンブラウザ : IE)
	alert(target.firstChild.nodeValue);
}
</script>
<p onclick="hello(event);">Hello, World!</p>

簡単かつ確実。
ただ、外部JavaScriptで完結した方がユーザビリティは高いと言われています。
# 使っちゃいけないってわけじゃないんですけど、HTML埋め込み型は定義するイベントが多いと大変だったり、JavaScript無効の環境への配慮が足りなくなりがちだったりと、面倒なことも多いです。

[デモ]DOM Scripting 標準ガイドブック Chapter5|factory.yusukenakanishi.com
http://factory.yusukenakanishi.com/javascript/practice/dom-scripting/005/


■リスナーを関数の外に出して、メモリリークを回避する
<p id="Hello">Hello, World!</p>
<script type="text/javascript">
function setHello(element){
	if(typeof element.addEventListener !== 'undefined'){
		element.addEventListener('click', hello, false);
	} else if(typeof element.attachEvent !== 'undefined'){
		attachEvent('onclick', hello);
	}
}
function hello(){
	// 保持すべきローカル変数がない!
	alert('Hello, World!');
}
setHello(document.getElementById('Hello'));
</script>

Microsoftから公式に開示されている解決法です。
DOMノードを参照するローカル変数element(DOM)は、setHello() で閉じており、hello() からは参照できません。
また、これはクロージャではないので安全です。
確実な方法なので、メモリリーク回避は基本的に「クロージャを使いがちなリスナー関数を外に出すこと」になると思います。

HTML ページの DOM オブジェクトへの循環参照はメモリ リークが発生します。
http://support.microsoft.com/kb/830555/ja


<p id="Hello">Hello, World!</p>
<script type="text/javascript">
(function(){	// エンクロージャ
	function setHello(element){	// クロージャ
		if(typeof element.addEventListener !== 'undefined'){
			element.addEventListener('click', hello, false);
		} else if(typeof element.attachEvent !== 'undefined'){
			attachEvent('onclick', hello);
		}
	}
	function hello(){	// クロージャ
		// 保持すべきローカル変数がない!
		alert('Hello, World!');
	}
	setHello(document.getElementById('Hello'));
})();
</script>

クロージャ(hello関数) からローカル変数element(DOM)を参照できないので、メモリリークしません。

13   名前: think : 2010/05/12(水) 02:12  ID:1kU6nkWR sub-gm
■addEvent() でメモリリークを回避する (1)
<p id="Hello">Hello, World!</p>
<p id="Cleanup">Cleanup all listener!</p>
<script type="text/javascript">
(function(){	// エンクロージャ

	function addEvent(element, eventType, listener, useCapture){	// クロージャ
		if(typeof element.addEventListener !== 'undefined'){
			element.addEventListener(eventType, listener, useCapture);
		} else if(typeof element.attachEvent !== 'undefined'){
			element.attachEvent('on' + eventType, listener);
		} else {
			return false;
		}
	}

	function removeEvent(element, eventType, listener, useCapture){	// クロージャ
		if(typeof element.removeEventListener !== 'undefined'){
			element.removeEventListener(eventType, listener, useCapture);
		} else if(typeof element.detachEvent !== 'undefined'){
			element.detachEvent('on' + eventType, listener);
		}
	}

	// "Hello, World!" を表示する
	function hello(event){	// クロージャ
		var target = event.target || event.srcElement;	// イベント呼び出し元ノード (モダンブラウザ : IE)
		alert(target.firstChild.nodeValue);
	}

	// 全てのイベントリスナを削除する
	function cleanup(){	// クロージャ
		removeEvent(document.getElementById('Hello'), 'click', hello, false);
		removeEvent(document.getElementById('Cleanup'), 'click', cleanup, false);
		removeEvent(window, 'unload', cleanup, false);
		alert('Cleanup all listener!');
	}

	// クリックすると、"Hello, World!" を表示する
	addEvent(document.getElementById('Hello'), 'click', hello, false);

	// クリックすると、全てのイベントリスナを削除する
	addEvent(document.getElementById('Cleanup'), 'click', cleanup, false);

	// window unload時に、全てのイベントリスナを削除する
	addEvent(window, 'unload', cleanup, false);

})();
</script>

よくある addEvent() です。
listenerを addEvent() の外から引っ張ってきているので、リスナー関数から変数element(DOM)にアクセスできず、循環参照しません。
window unload時に、全てのイベントリスナを削除しているのは念のためです。(リークパターンは他にもあるようなので)
なかなか便利ですが、attachEventがaddEventListenerの全機能を網羅できない点に注意する必要があります。

- addEventListenerは同じイベントリスナを連続して追加すると追加した順番で実行してくれるが、attachEventは実行順が不定。
- リスナーの第一引数に格納されるeventオブジェクトのプロパティ、メソッドに違いがあり、addEventListenerしか対応できないものも存在する。
- addEventListenerには「第三引数useCapture」(イベントバブルに反応しないイベントキャプチャを実行する)があるが、attachEventにはない。

element.addEventListener - MDC
https://developer.mozilla.org/ja/DOM/element.addEventListener
element.removeEventListener - MDC
https://developer.mozilla.org/ja/DOM/element.removeEventListener

attachEvent
http://msdn.microsoft.com/en-us/library/ms536343%28VS.85%29.aspx
detachEvent Method (A, ABBR, ACRONYM, ...)
http://msdn.microsoft.com/en-us/library/ms536411%28VS.85%29.aspx
VEMap.DetachEvent メソッド
http://msdn.microsoft.com/ja-jp/library/bb412537.aspx

Document Object Model Events - 1.4 Eventインタフェース
http://www.y-adagio.com/public/standards/tr_dom2_events/events.html#Events-interface
Document Object Model Events - 1.2.2. イベント捕獲
http://www.y-adagio.com/public/standards/tr_dom2_events/events.html#Events-flow-capture

event - MDC
https://developer.mozilla.org/ja/DOM:event
event.target - MDC
https://developer.mozilla.org/en/DOM/event.target
event.currentTarget - MDC
https://developer.mozilla.org/en/DOM/event.currentTarget
event.charCode - MDC
https://developer.mozilla.org/en/DOM/event.charCode
event.eventPhase - MDC
https://developer.mozilla.org/en/DOM/event.eventPhase
event.preventDefault - MDC
https://developer-stage.mozilla.org/ja/DOM/event.preventDefault

event
http://msdn.microsoft.com/ja-jp/library/cc427885.aspx
event.srcElement
http://msdn.microsoft.com/ja-jp/library/cc410014.aspx
event.returnValue
http://msdn.microsoft.com/ja-jp/library/cc409956.aspx
event.cancelBubble
http://msdn.microsoft.com/ja-jp/library/cc409795.aspx
event.keyCode
http://msdn.microsoft.com/ja-jp/library/cc391971.aspx

JavaScriptの動かないコード (中級編) 動的追加したイベントの実行順序 ( addEventListener vs attachEvent ) - 主に言語とシステム開発に関して
http://d.hatena.ne.jp/language_and_engineering/20081011/1223680300
addEventListenerとattachEventでは実行される順番が違う at HouseTect, JavaScript Blog
http://hisasann.com/housetect/2008/09/addeventlistenerattachevent.html

14   名前: think : 2010/05/12(水) 02:12  ID:1kU6nkWR sub-gm
■addEvent() でメモリリークを回避する (2)
<p id="Hello">Hello, World!</p>
<p id="Cleanup">Cleanup all listener!</p>
<script type="text/javascript">
(function(){	// エンクロージャ

	function convertAttachEventListener(listener){	// クロージャ
		return function(event){
			event.target = event.srcElement;	// Returns the target to which the event was dispatched.
//			event.currentTarget = this;	// Identifies the current target for the event, as the event traverses the DOM.
			event.charCode = event.keyCode;	// Returns the Unicode value of a character key pressed during a keypress  event.
//			event.eventPhase = event.srcElement==this ? 2 : 3;	// Indicates which phase of the event flow is currently being evaluated.
			event.stopPropatation = function(){ event.cancelBubble = true; };	// Cancel Event Bubble
			event.preventDefault = function(){ event.returnValue = false; };	// Cancel Default Action
			return listener(event);
		};
	}

	function addEvent(element, eventType, listener, useCapture){	// クロージャ
		if(typeof element.addEventListener !== 'undefined'){
			element.addEventListener(eventType, listener, useCapture);
		} else if(typeof element.attachEvent !== 'undefined'){
			listener = convertAttachEventListener(listener);
			element.attachEvent('on' + eventType, listener);
		} else {
			return false;
		}
		return listener;
	}

	function removeEvent(element, eventType, listener, useCapture){	// クロージャ
		if(typeof element.removeEventListener !== 'undefined'){
			element.removeEventListener(eventType, listener, useCapture);
		} else if(typeof element.detachEvent !== 'undefined'){
			console.log('detachEvent');
			console.log(element);
			console.log(eventType);
			console.log(listener);
			element.detachEvent('on' + eventType, listener);
		}
	}

	// "Hello, World!" を表示する
	function hello(event){	// クロージャ
		var target = event.target;	// IE8以下でも event.target を使える!
		console.log(target.id);
		alert(target.firstChild.nodeValue);
	}

	// 全てのイベントリスナを削除する
	function cleanup(){	// クロージャ
		removeEvent(document.getElementById('Hello'), 'click', helloListener, false);
		removeEvent(document.getElementById('Cleanup'), 'click', cleanupListener, false);
		removeEvent(window, 'unload', unloadListener, false);
		alert('Cleanup all listener!');
	}

	// クリックすると、"Hello, World!" を表示する
	var helloListener = addEvent(document.getElementById('Hello'), 'click', hello, false);

	// クリックすると、全てのイベントリスナを削除する
	var cleanupListener = addEvent(document.getElementById('Cleanup'), 'click', cleanup, false);

	// window unload時に、全てのイベントリスナを削除する
	var unloadListener = addEvent(window, 'unload', cleanup, false);

})();
</script>

attachEventのeventオブジェクトをaddEventListener互換のプロパティ&メソッドに変換した addEvent() です。
convertAttachEventListener() で listener をaddEventListener互換に変換しています。
listener関数の中で処理を分岐させる手間を省けますが、全てのプロパティに対応していませんし、attachEventの他の仕様(イベントの実行順が不定など)は引き継いでいます。

addEventでリスナーを返しているのは、attachEventする時のリスナーが変更されているためです。
例えば、addEventに hello() を渡すと、attachEventの分岐処理で hello() を変換して新しいリスナーを作るので、detachEvent で hello() をリスナーとして渡しても失敗してしまいます。

15   名前: think : 2010/05/12(水) 02:12  ID:1kU6nkWR sub-gm
■初めはいろいろ間違っていました
まず、始めにごめんなさい。
>>0-7 の私の発言はいろいろと間違っていました。
正しい箇所よりも間違っている箇所が多いので、読み流すようお願いします…。m(_ _)m


■クロージャによるメモリリーク回避方法 まとめ

メモリリーク回避パターンはいろいろありましたが、基本的に「リスナー関数を外に出せば、メモリリークを回避できる」と結論づけられると思います。
個人的に優先順位を付けるならば、以下の順番になります。

1. attachEvent(), addEventListener() にクロージャを渡さない。
2. HTMLのイベント属性に関数を入れ、クロージャを使わない。
3. element = null; でDOMノードへの参照を切る。

1. 及び 2. は堅実な方法だと思います。(今まで紹介した解決法は全てこれらに該当します)
外部JavaScriptだけで完結させたい場合は 1. を使い、「多少はHTMLを汚してもいい」と割り切るなら 2. を使う、という使い分けになるでしょうか。

3. を紹介しなかったのは、参照の切り忘れが怖いからです…。
element = null; はなくても動作するものなので、うっかり入れ忘れてしまうことはあり得ます。


■クロージャ以外にもメモリリークパターンが存在する
MSDNによると、4つのメモリリークパターンが存在します。
1. 循環参照
Internet Explorer の COM インフラストラクチャと任意のスクリプティング エンジンの間で相互参照がカウントされている場合は、オブジェクトによってメモリ リークが発生することがあります。これは最も明白なパターンです。

2. クロージャ
クロージャは、既存の Web アプリケーション アーキテクチャに対して最大のパターンをもたらす特殊な形式の循環参照です。
クロージャは、特定の言語キーワードに依存しており、包括的に検索できるため、簡単に特定できます。

3. クロスページ リーク
クロスページ リークは、サイトからサイトに移動するときに発生する内部的なブックキーピング オブジェクトのリークで、通常は非常に小さいものです。
DOM 挿入順序の問題に加えて、コードにわずかな変更を加えることでこれらのブックキーピング オブジェクトの作成を防ぐ方法を示す対処方法を検証します。

4. 擬似リーク
これらは実際にはリークではありませんが、メモリの経過を理解していない場合には非常に面倒な場合があります。
スクリプト要素のリライトと、そのリライトが必要に応じて実際に実行される際にごくわずかなメモリがリークしていくようすについて検証します。
http://msdn.microsoft.com/ja-jp/library/bb250448

今回は、「2. クロージャ」だけを解説しています。


■質問の締め切りはもう少し後に

内容は出来る限りチェックしていますが、アドバイスがありましたら指摘していただければ有り難いです。
しばらくして、レスが落ち着いてきたら(あるいはなければ)、質問を締め切りたいと思います。

16   名前: ai : 2010/05/12(水) 02:12  ID:fyBBmsDf sub-Ax
発言しようかどうしようかすごく迷ったんですが…

まず、最初に思ったのが、
「おぉぅ……すげ〜。thinkさんのおかげで皆幸せになれそうだ^^」
あと、
「あ、document, window はがっつりクロージャ作っていいんだ。」っていう安心。
(domとスクリプトの別世界をまたいでの循環参照を作らないように気をつけるのはもちろんのこと。)



一応、
>>10
>クロージャが参照を保持する変数elementは null で初期化された時点で「スクリプトエンジンが管理するオブジェクト」となります。
の補足(?)ただし、あくまで私個人の考えなので間違っているかもしれません。
function hoge() {	// エンクロージャ
	var element = document.getElementById('Test');	// ローカル変数(DOM)

	element.attachEvent('onclick', function(event){	// クロージャ
		// element(DOM) → onclick(DOM) → Function(script) → element(script)
		// 変数element(script)への参照を保持する
	});
}
の場合、
+---------+                        +------------------+
| element | ---------参照--------> | DOM オブジェクト |
+---------+                        +------------------+
    ↑                                      |
   参照                                    参照
    |                                      ↓
+----------------------+               +---------+
| Functionオブジェクト | <----参照---- | onclick |
+----------------------+               +---------+
という図になる。(等幅で表示されない方、ごめんなさい汗。)
ホントはもっと細かい図になるみたいですが、おおまかに書くとこんな感じだと思います。違ってたらごめんなさい汗
まさしく参照がループ(循環)されている。
※domとスクリプトの別世界をまたいでの循環参照。

ここで、hoge 関数の最後に
element = null;
を加えると、
+---------+               +------+     +------------------+
| element | ----参照----> | null |     | DOM オブジェクト |
+---------+               +------+     +------------------+
    ↑                                      |
   参照                                    参照
    |                                      ↓
+----------------------+               +---------+
| Functionオブジェクト | <----参照---- | onclick |
+----------------------+               +---------+
element の参照先を変えてやる( 主に null 。別に Array オブジェクトでも何でもいいと思う。dom オブジェクトさえ参照していなければ。) ことによって、
ループされていた図が null と domオブジェクトの間でぷっつり切れた状態になる。
つまり、ループされなくなって、あとは自動的にメモリの解放をやってくれる。
…んじゃないかなーっていう個人の考え。



で、次に、疑問に思ったことですが、
>>11
function hoge() {	// エンクロージャ
	var element = document.getElementById('Test');	// ローカル変数(DOM)

	document./*@cc_on @if(@_jscript_version<=5.8) attachEvent('on' + @else @*/addEventListener(/*@end @*/ 'click', function (event) {	// クロージャ
		// document(DOM) → onclick(DOM) → Function(script) → element(DOM)
		// 変数element(DOM)への参照を保持する
	},false);
}
はメモリリークなのでしょうか?
domとスクリプトの別世界はまたいではいるけど、循環参照なのでしょうか?
それとも、
document -> onclick -> function -> element -> element.ownerDocument -> document
によって循環参照?いやいや、documentなら大丈夫だし…。
document が解放されるなら、document.onclick も解放され、次にfunctionオブジェクトが解放され、
それでもって、element はfunctionオブジェクトが解放されたためにどこからも参照されなくなったので解放される。
…んじゃないかなー、と。

17   名前: think : 2010/05/12(水) 02:12  ID:1kU6nkWR sub-gm
>>16
補足ありがとうございます。
図にしてみると、わかりやすいですね!

> ホントはもっと細かい図になるみたいですが、おおまかに書くとこんな感じだと思います。違ってたらごめんなさい汗
「DOMオブジェクトが onclick を参照する」というところにちょっと違和感を感じます(DOMオブジェクトの方が広義に感じます)が、多分私の理解と大差ないです。
以降、説明に図を取り入れてみようと思いました。

> document が解放されるなら、document.onclick も解放され、次にfunctionオブジェクトが解放され、
これは解放順が逆なんじゃないかなーと思います。
つまり、functionオブジェクトが解放され、document.onclick も解放され、documentが解放される。
で、functionオブジェクト解放時にDOMオブジェクトを参照しているローカル変数を保持し続けていると、解放できなくなってメモリリークしてしまう。

この辺りは、>>9及び、「>>9で引用しているMSDN」を読むと理解できるんじゃないかと思います。

実は、私も最初は誤解していて、babu_babu_babooさんに指摘されるまで気が付きませんでした。
「循環参照」というキーワードから入ると誤解を生む気がするので、別の切り口から説明してみたいと思います。

18   名前: think : 2010/05/12(水) 02:12  ID:1kU6nkWR sub-gm
■関数とクロージャ

「ECMAScriptにクロージャという定義はない」と聞いたことがありますが、便宜上ここでは内包関数をクロージャと呼びます。(>>9に同じ)
<script type="text/javascript">
function hoge(){
	var n = 0;	// ローカル変数
	alert(n);
	// 関数呼び出し完了後にローカル変数は全て解放される
}
hoge();	// 0
hoge();	// 0
hoge();	// 0
</script>

通常の関数では内部で初期化されたローカル変数は関数呼び出し時に初期化され、処理完了後に全て解放されます。
ですので、この場合のローカル変数についてはメモリリークを考えなくて大丈夫。関数呼び出しに応じて、メモリを確保して解放するからです。
言い換えると、「エンクロージャでなければ、null による後始末(初期化)は要らないよ。」ってことです。
<script type="text/javascript">
function hoge(){	// エンクロージャ
	var n = 0;	// ローカル変数
	return function(){	// クロージャ
		// ローカル変数nへの参照を保持する
		alert(++n);
	}
}
var foo = hoge();	// クロージャを参照する
foo();	// 1
foo();	// 2
foo();	// 3
</script>

クロージャ(内包関数)が出来た時点でローカル変数nはクロージャが参照を保持するようになります。
つまり、foo() 呼び出し完了後もローカル変数nは解放されず、ページが破棄されるまで参照し続けます。

問題はページ破棄時にメモリから解放される順番です。
ローカル変数n はクロージャから参照され続けているので、まずクロージャから解放します。
次にローカル変数nを解放し、hoge() を解放する。

…という処理順を期待するわけですが、クロージャはローカル変数nへの参照を保持しているので、クロージャを解放する前にローカル変数nへの参照を切らなくちゃいけない、という問題があります。
でも、大丈夫!
スクリプトエンジンがちゃんと解決してくれます。(どんな処理になっているのか、は私にもわかりません)

問題は異なるエンジン間で参照している時にメモリから解放する順番です。

スクリプトエンジンから「DOMプロセッサが管理するオブジェクト」は管理できず、DOMプロセッサから「スクリプトエンジンが管理するオブジェクト」は管理できない。

それぞれ管轄外のオブジェクトが存在するわけです。おそらく、それほど柔軟には対応できていません。



ここまでは>>9を書くときに参考にしたMSDN(http://msdn.microsoft.com/ja-jp/library/bb250448)及び、<さんの説明に基づいたものです。
ここから先は、私の想像も入っています。(おそらく、大きく的を外してはいないと思いますが、間違っていたらごめんなさい)
function hoge() {	// エンクロージャ
	var element = document.getElementById('Test');	// ローカル変数(DOM)

	document./*@cc_on @if(@_jscript_version<=5.8) attachEvent('on' + @else @*/addEventListener(/*@end @*/ 'click', function (event) {	// クロージャ
		// document(DOM) → onclick(DOM) → Function(script) → element(DOM)
		// 変数element(DOM)への参照を保持する
	},false);
}

これは参照している順番とは逆方向に解放していく、という動作になると思います。
つまり、
element(DOM) → Function(script) → onclick(DOM) → element(DOM)

という順番で解放していきます。
問題は「element(DOM) がエンクロージャ(関数hoge)で初期化されている」ということです。
おそらく、順番としては element(DOM) より先にクロージャを解放することを期待すると思います。
すると、解放処理で循環してしまいます。

1. element(DOM) は関数hogeが解放される直前に解放される。
2. クロージャは element(DOM) が解放された後に解放する。
3. onclick はクロージャが解放された後に解放する。

これでは、永遠に解放できませんよね。

先程のクロージャでなぜこの問題が発現しないかというと、スクリプトエンジンが管理しているオブジェクトで完結しているからで、解放処理が循環していようとも何とか解決できます。
ところが、「スクリプトエンジン以外が管理しているオブジェクト」に参照しているローカル変数があると、その参照はスクリプトエンジンの管轄外なので解放できなくなります。

ここで、
element = null;

を入れるとなぜ解決できるか、というと、クロージャ解放時に参照しているオブジェクトが全て「スクリプトエンジンが管理するオブジェクト」なので解放処理で循環があっても問題なく解放できる。

…と私は理解しています。

19   名前: think : 2010/05/12(水) 02:12  ID:1kU6nkWR sub-gm
■管轄外のオブジェクト間でのクロージャによる循環参照 図解
function hoge() {	// エンクロージャ
	var element = document.getElementById('Test');	// ローカル変数(DOM)

	element.onclick = function(event){	// クロージャ
		// element(DOM) → onclick(DOM) → Function(script) → element(DOM)
		// 変数element(DOM)への参照を保持する
	};
}

上記コードの循環参照を図で表すと、以下のようになります。
+------------------------------------------+
|                                          |
| スクリプトエンジンが管理するオブジェクト |
|                                          |
| +----------+               +---------+   |
| | Function |-----参照----->| element |   |
| +----------+               +---------+   |
+------------------------------------------+
       ↑                        |
      参照                      参照
       |                        ↓
+------------------------------------------+
| +---------+             +--------------+ |
| | onclick |             | Element Node | |
| +---------+             +--------------+ |
|                                          |
|   DOMプロセッサが管理するオブジェクト    |
|                                          |
+------------------------------------------+

管轄の異なるオブジェクト間で循環参照しているのがポイント。
4つのオブジェクトが循環参照しているわけではありません。



ページ破棄時に期待される解放処理の循環を図で表すと、こうなります。
+------------------------------------------+
|                                          |
| スクリプトエンジンが管理するオブジェクト |
|                                          |
| +----------+               +---------+   |
| | Function |<-----解放-----| element |   |
| +----------+               +---------+   |
+------------------------------------------+
       |                        ↑
      解放                      解放
       ↓                        |
+------------------------------------------+
| +---------+             +--------------+ |
| | onclick |             | Element Node | |
| +---------+             +--------------+ |
|                                          |
|   DOMプロセッサが管理するオブジェクト    |
|                                          |
+------------------------------------------+

上図の通りに解放されればいいのですが、
クロージャを使うと、Functionから解放しなければならないのでこの処理は成立しません。



ここで element を nullで初期化すると、
function hoge() {	// エンクロージャ
	var element = document.getElementById('Test');	// ローカル変数(DOM)

	element.onclick = function(event){	// クロージャ
		// element(DOM) → onclick(DOM) → Function(script) → element(DOM)
		// 変数element(DOM)への参照を保持する
	};
	element = null;	// DOMノードへの参照を切る
}

この瞬間、element は「Element Node(DOM)」を参照しなくなるので、図で表すと以下のようになります。
+------------------------------------------+
|                                          |
| スクリプトエンジンが管理するオブジェクト |
|                                          |
| +----------+               +---------+   |
| | Function |<-----解放-----| element |   |
| +----------+               +---------+   |
+------------------------------------------+
       |
      解放
       ↓
+------------------------------------------+
| +---------+                              |
| | onclick |                              |
| +---------+                              |
|                                          |
|   DOMプロセッサが管理するオブジェクト    |
|                                          |
+------------------------------------------+

実際には、Functionの解放が先かもしれませんが、とにかくスクリプトエンジン内での解放処理は問題なく行えます。
また、window unload時に element.onclick = null; で参照を切ると、それぞれの管轄で解放処理を行えるので循環しなくなります。


■「スクリプトエンジンが管理するオブジェクト」によるクロージャ 図解
<script type="text/javascript">
function hoge(){	// エンクロージャ
	var num = 0;	// ローカル変数(Script)

	var foo = function(){	// クロージャ
		// foo(Script) → Function(Script) → num(Script)
		// ローカル変数numへの参照を保持する
		alert(++num);
	};
	return foo;
}
var foo = hoge();	// クロージャを参照する
foo();	// 1
foo();	// 2
foo();	// 3
</script>

図で表すと以下のように。
+------------------------------------------+
|                                          |
| スクリプトエンジンが管理するオブジェクト |
|                                          |
| +----------+                     +-----+ |
| | Function |---------参照------->| num | |
| +----------+                     +-----+ |
|      ↑                                  |
|     参照                                 |
|      |                                  |
|   +-----+                                |
|   | foo |                                |
|   +-----+                                |
+------------------------------------------+

解放処理は逆順になるだけなので省略。
スクリプトエンジンの中で閉じていれば、クロージャがどんなオブジェクトへの参照を保持していようとも、問題なく解放できます。

一覧へ戻る