再利用できるクロージャを使ったWebアプリケーション開発

2015.6.2 追記

このようにオブジェクトが頻繁に生成/破棄される状態を「メモリ撹拌」と呼びます。アプリケーション実行時にメモリ撹拌が発生するのを避けることで、GCの処理時間を削減することができるのです。

いろいろ書いたけど、クロージャについてメモリ撹拌の削減を行うテクニック、ということにします。

大規模開発ではクロージャは駄目!

百花繚乱の js フレームワークやライブラリですがそのソースコードを眺めていくと、再利用可能クロージャ ( reusable closure ) を目にすることが増えました。

今回は、そんな再利用可能クロージャについてご紹介したいと思います。

ところで大規模開発ではクロージャ駄目!絶対!とはしばしば言われることです。

最近は、ブラウザの実装がこなれてきたことへの安心感か、腐ったブラウザと格闘した世代が開発から離れたのか、目くじら立てて言われることも少ないように思います。

Web 開発者の集まりなどでそんな話をしますと怪訝な目をされてしまうこともあるので、アッチョンブリブリケです。

クロージャのデメリット

一応デメリットについてあげてみます。

  1. 関数スコープのコストが掛かかってしまう
  2. DOM と循環参照していないか?直感的に分かりづらい、働きづめのヒートアップした頭では本当によく分からない、勘弁してください

クロージャの良くない副作用として、Internet Explorer で簡単にメモリリークを発生させることがあります。

クロージャでは、思わぬメモリリークが容易に発生します。

使っても良い. ただし慎重に.

ただし一点注意すべき点は, クロージャはその閉じたスコープへのポインタを保持しているという点です. そのため, クロージャを DOM 要素に付加すると循環参照が発生する可能性があり, メモリリークの原因となります.

“JavaScript 「再」入門”“Google JavaScript Style Guide 和訳” は、Javascript 開発全般についてよくまとまった記事ですので、目を通しておくと良いです。

クロージャのコスト

みんな大好き jQuery の作者ジョン・レシグの著した『Javascript 忍者の極意』では、一章を裂いてクロージャの利用を勧めています。またクロージャをうまく使えていないケースがある、と指摘しています。

一方で、クロージャのコストについては、次に示す一箇所での言及に留まっています。

しかし大規模開発では特に留意しなくてはいけない点に思います。

ここでひとつ指摘しておくべき、重要な注意事項がある。この構造のすべてを、どこかで見ることができるわけではない。これらの情報すべてを包む「クロージャ」オブジェクトが存在するわけではないのだ。それでも、この方法で情報を収納し参照するには、直接的なコストがかかる。クロージャを介して情報をアクセスする関数は、いわば重い「足かせ」によって、それらの情報をひきずっているのだ。信じられないほど便利なクロージャだが、その代償として、オーバーヘッドがかかる。すべての情報をメモリに保存しなければならないし、そのメモリが、もう本当に不要だと(ガンベージコレクションを行っても安全だと)Javascriptエンジンが確信するまで(あるいはページがアンロードされるまで)そのメモリで情報を保存しなければならないのだ。

さて、次は2008年ですが具体的なクロージャのコストについて実際に計測されていて素晴らしい記事です。

是非目を通しておきましょう、Web アーカイブになります。

今回はカプセル化を先のような方法で行うとどのようなコストがかかるのかということ探ってみました。

次に生成1つあたりのメモリコストの違いですが これはでかいですね。これを計算してみると1つあたりのメモリコストは1,110 bytesとなっているようです。

循環参照とガベージコレクション

富豪的なプログラミングに目を瞑るなら、最大の難点は循環参照を作っていないか気を配らなくてはいけない、という点です。

これ自体ナイーブな問題ですが首尾よく解決したとして、続いてブラウザのガベージコレクションに目を移しますと…

その能力はブラウザ毎にまちまちでページを離れるまでガベージコレクションされないような挙動のブラウザも報告されています。

また、今後もテストの網を潜り抜け問題のあるブラウザがリリースされないとも限りません。

そして、対応ブラウザのすべてについてガベージコレクションが健全か?調べるよりは、ちゃっちゃと再利用可能クロージャを使ってしまうのが最新の流行のようですよ。

クロージャを使うケース

本題に行く前に、積極的にまたは安全にクロージャを使うケースを上げてみます。

  • スコープに新しい名前を追加したくない場合で関数が破棄されることが明らかなとき
  • blog 等の参考コード
  • 実装の不備を回避するため使うのが妥当なケース
    1. IE独自イベントモデル
    2. IE8- と Opera11- の XHR と MSXML
    3. setInterval, rAF 等のタイマー系は?
ケース1・IE独自イベントモデル

IE5 ~ IE8 の独自イベントモデルに於いて、イベントオブジェクトに event.currentTarget に相当するものがなく、現在どの HTML 要素でイベントが起こっているか分かりません。そのため HTML 要素とコールバック関数を束縛するクロージャを使う必要があります。
『Javascript 第5版』 オライリー p430 17.3.6 attachEvent() と this キーワード

ケース2・IE8- と Opera11- の XHR と MSXML

IE8 以下と Opera11 以下の XHR (と MSXML) ではイベントオブジェクトが用意されないため、Event.type が分からない不備があります。

このためにイベントタイプとコールバック関数を束縛するクロージャを使用するのが適当かもしれません。

ケース3・setInterval, rAF 等のタイマー系は?

また、setInterval, setTimeout, requestAnimationFrame, setImmediate などのタイマー系については関数だけを登録できてコールバック時の this コンテキストは window になってしまいます。

これらタイマー系については、複数のタイマーを登録しても Web ブラウザに対してはひとつのタイマーだけを登録する高速化手法がありますので、その際に合わせて this コンテキストを設定できるようにしてしまうのが綺麗でしょう。

現在 amachang 氏の書いたコードを拝見することはできませんが、sawat 氏によるデモは公開されていてタイマーをまとめる効果を実感できます。

無駄に多くのsetIntervalを使ってアニメーションをすると、setInterval03.jsを使わない場合は、Firefoxだとかなりグダグダな感じになってしまう。しかし、setInterval03.jsを使うとほぼ期待通りの間隔で実行させることができるみたいだ。

再利用可能クロージャ

いよいよ本題に入ります。一般的なクロージャと再利用可能クロージャを見比べてみます。

this コンテキストを縛る一般的なクロージャ

以下にコールバックの this コンテキストを上書きする、というよくあるクロージャを示します。

function createCallback( func, context ){
 return function(){
  return func.apply( context, arguments );
 };
};

再利用可能クロージャのサンプル

function createCallback( cbHash ){
 return function(){
  return cbHash.func && cbHash.func.apply( cbHash.context, arguments );
 };
};

これが、再利用可能クロージャの最も簡単な例になります。一般的なクロージャのサンプルコードと大きく違いはありません。

createCallback に対して、コンテキストとコールバック関数ではなく、それらを格納したハッシュ cbHash を与えているのが唯一の違いです。

しかしこの変更によって cbHash のメンバーを適宜に書き換えることで一旦不要になったクロージャを再利用することができます。

以上を基本に、各ライブラリ・フレームワークの特徴にあわせた味付けがされています。

実用的な再利用可能クロージャの例

次が実際の再利用可能クロージャのコード例になります。create/collect メソッドだけで再利用可能クロージャの操作ができます。

クロージャの回収と再利用や cbHash の操作が隠蔽されていることをご確認ください。

var ReusableClosure = (function(){
 var POOL = []; // pool cb and cbHash key
 
 function createCallback( cbHash ){
  return function( arg0 ){
   if( arg0 === POOL ) return cbHash;
   return cbHash.func && cbHash.func.apply( cbHash.context, arguments );
  };
 };
 
 return {
  create : function( context, func ){
   var ret = POOL.pop(), cbHash;
   
   if( ret ){
    cbHash = ret( POOL );
    cbHash.context = context;
    cbHash.func = func;
    return ret;
   };
   
   return createCallback( { context : context, func : func } );
  },
  collect : function( cb ){
   var cbHash = cb( POOL );
   cbHash.context = cbHash.func = null;
   POOL.push( cb );
  }
 };
})();

// 生成
var cb = ReusableClosure.create( myObject, readyHandler );

// 破棄
ReusableClosure.collect( cb );

最後に

“再利用可能クロージャを目にすることが増えました”“ちゃっちゃと再利用可能クロージャを使ってしまうのが最新の流行”という記述がありますが、僕の知る限り使われているのは、pettanR だけでした。ちょっと盛りました。てへ、ペロ。

でも、僕の職場では pettanR は大変著名な js フレームワークで、木曜の飲み会でもその話題で持ちきりでしたよ。

また、記事のそこここで断定口調を使っていますが、「僕は~と思う」、に適宜に読み替えてください。

このような胡散臭い記事を鵜呑みにしてしてはいけません、ではでは。

参考記事