【すっきり】JavaScriptのクロージャのしくみ。

2016/6/5

要点。

JavaScriptの関数は変数に代入できる。

var asFunc = function () { // 関数を定義して代入。
    var data = 0;
    ...
}

そして実行時に生成されたローカル変数も一緒に変数に代入できる。

var asClosure = asFunc(); // 関数を実行し、生成されたローカル変数もろとも代入。

これがクロージャである。

関数CODE
クロージャCODE + DATA

サンプル。

もっともこれではDATAの存在を確かめようがないのでサンプルを作ってみる。

var asFunc = function f1() {
  var data = 0;
  return function f2() {
      return ++data;
  };
};

// 1. func1を関数として毎回新規に実行。
alert(asFunc());          // returns f2  (f1実行 → DATA生成 → DATA破棄)
alert(asFunc()());        // returns 1   (f1実行 → DATA生成 → f2実行 → DATA破棄)
alert(asFunc()());        // returns 1   (f1実行 → DATA生成 → f2実行 → DATA破棄)
alert(asFunc()());        // returns 1   (f1実行 → DATA生成 → f2実行 → DATA破棄)

// 2. func1をクロージャ化してDATAを保持しつつ実行。
var asClosure = asFunc(); // returns f2  (f1実行 → DATA生成)
alert(asClosure);
alert(asClosure());       // returns 1   (f2実行)
alert(asClosure());       // returns 2   (f2実行)
alert(asClosure());       // returns 3   (f2実行)

f1が返すf2によって、f1のローカル変数にアクセスできる。 よく見るサンプルだがこのような使い方は実用的じゃない。トリッキーでわかりにくい。

本来の使われ方。

では何のためのクロージャなのか? 非同期処理でコールバックを書くときにこれほど便利なものはない。

function apiCall() {
    var url = "...";
    httpRequest(url,                        // 何かの非同期処理。
        function onComplete(status, body) { // コールバック。
            parse(url, status, body);       // ここからurlにアクセス可。
            ...
        }
    );
}

apiCall()を抜けた後に、onComplete()がコールバックされても、urlにアクセス可能。 便利な一方で、どこか気持ち悪い。腑に落ちないものがあった。

気持ち悪さの理由。

それは、C言語などの感覚では一旦apiCall()を抜けたらauto(=stack)変数のurlは存在しないから。 コンパイラやインタープリタの仕組みを知っていて、中の動きを想像しながらコードを書くならなおさらだ。

しかしJavaScriptではこれが許される。それはJavaScriptではauto=heapだからだ。 ここがわかれば全ての謎が氷解する。

ついでに Objective-C の blocks

同じようにObjective-Cのblocksも便利である。ただしC言語なので基本はauto=stack。コールバック内から親の変数にアクセスする場合は __blocks 属性を付けてheapに割り当てる必要がある。

blocksを呼ぶ側にも注意が必要。blocksポインタはstackにほったらかしにせず、heapやinstanceにコピーしてstaticに保持する。そうしないとblocks自体が解放されてしまって、後から呼び出したときに EXC_BAD_ACCESS が起きる。



©