あどけない話

Internet technologies

クロージャ

JavaScriptλ式の関係について書こうと思ったのですが、その前にクロージャについてお話しする必要があることに気付きました。

僕が最初にクロージャという言葉を知ったのは、大学の学部生のときです。Lisp の勉強をしていて出てきたのです。でも、まったく理解できませんでした。

これまで僕が主に使ってきた言語は、C や Emacs Lisp です。C では関数の中に関数を定義できないのでクロージャはありませんし、Emacs では動的スコープでクロージャはありませんから、クロージャに関する知識がなくても困りませんでした。それが、クロージャを本気で理解しなかった理由だと思います。

ここでは、JavaScript にとってクロージャが必要である例を示すことにしましょう。必要性が実感できれば、詳しく勉強しようという意欲が湧くはずだからです。

関数に static な変数

C言語には、関数に static な変数を定義できます。たとえば、デバッグや最適化のときに、ある関数が何回呼ばれたかを知る場合に便利です。以下に例を示します。

#include <stdio.h>

func() {
  static int counter = 0;
  printf("%d\n", ++counter);
}

main() {
  int i;
  for (i = 0; i < 5; i++)
    func();
}

関数に static な変数のことをよく知らないと、counter は実行時に毎回 0 に初期化され、5 回とも 1 が表示されるように思います。

しかし、この static の文は、関数の定義の際に評価されるだけで、実行時には評価されません。だから、前の値がずっと残り、1, 2, 3, 4, 5 と表示されます。

関数に static な変数を学んだときは、違和感を覚えてギョッとしたはずです。

クロージャ

もし、static というキーワードがない言語で、同じことを実現するにはどうしたらいいでしょうか?

もちろん、グローバル変数を使えば実現できます。しかし、グローバル変数は、モジュール間の結合力を強めてしまうので、あまりお勧めできません。

クロージャを使えば、クローバル変数を使わずに、これを実現できます。上記と同じ例を JavaScript で書くとこうなります。

var func = (function() { // 外側の関数
    var counter = 0;
    return function() { // 内側の関数
        console.log(++counter);
    }
})();

function main() {
    for (var i = 0; i < 5; i++) {
	func();
    }
}

関数 main の定義はいいでしょう。

でも、関数 func の定義を見て、いろいろギョッとすると思います。

  • 無名関数が使われている
  • 無名関数が実行されている
  • 関数の中から関数が返されている

無名関数に関しては、説明しません。ここで重要なのは、関数の中で関数を定義していることです。

外側の関数が中で関数を定義し、その内側の関数を返します。これに func という名前が付くということが分かれば十分です。実際、関数 func の実際の定義を知るために、func.toString() を実行して文字列に直すと、以下のようになっていることが分かります。(document.write() では、いろいろ問題があるので、Firebug の console.log() を利用しています。)

function() {
    console.log(++counter);
}

再びギョッとするでしょう。クロージャを知らなければ、counter がグローバル変数に見えるからです。JavaScript では変数を定義した環境を使って解決します。環境というのは、外側の関数のことです。

ですから、この counter という変数の実体は、定義した際、外側に位置する関数のローカル変数である counter になります。決してグローバル変数ではありません。

このように、関数の中で関数を定義すると、外側の関数が「閉じ込めるもの」、すなわちエンクロージャとなり、内側の関数が「閉じたもの」、すなわちクロージャとなるわけです。別の言い方をするなら、クロージャとは定義されたときの環境を持ち合わせている関数のことです。

無名関数のことをクロージャと言ったり、多段になってない単なる関数をクロージャと言っている人がいますが、それは厳密な意味では間違いです。

クロージャの用途

上記のだけで、クロージャが役に立つことは明らかですが、さらに典型的な例を挙げておきましょう。

あるインスタンス関数から、タイマーを指定したいとします。こんな風にです。

var Cls = Class.create({
    initialize: function(counter) {
	this._counter = counter;
    },
    run: function() {
	setInterval(function() {
	    this._counter++; // この this は危険
	    console.log(this._counter); // この this は危険
	}, 1000);
    }
});

このコードを (new Cls(0)).run() のように実行すると、Firebug の console には、NaN が表示され続けます。

どうしてかというと、タイマーを扱うのは window オブジェクトですから、関数 run の中で定義した関数を呼び出すのは window オブジェクトになります。なので、this が指すオブジェクトは window なのです。(実装によって違うかもしれません。)

このようにイベントハンドラーに渡した関数の中では、this が思い通りの値になりません。これを回避するには、クロージャを使い、this の値を退避しておきます。退避先の変数名には、self を使うことが多いようです。

var Cls = Class.create({
    initialize: function(counter) {
	this._counter = counter;
    },
    run: function() {
	var self = this; // ここが重要
	setInterval(function() {
	    self._counter++; // this ではなく self を使う
	    console.log(self._counter); // this ではなく self を使う
	}, 1000);
    }
});

なお、prototype.js の bind() を使えば、もう少し直感的にこの問題を解決できます。

var Cls = Class.create({
    initialize: function(counter) {
	this._counter = counter;
    },
    run: function() {
	setInterval(function() {
	    this._counter++;
	    console.log(this._counter);
	}.bind(this), 1000); // ここが重要
    }
});

Pascal と C の論争

昔、Pascal と C はどちらが優れているかという論争が繰り返された時代がありました。もう Pascal のことを忘れてしまった人も多いと思いますので、まず客観的な事実を挙げておきます。

  • Pascal は関数の中で関数を定義できるが、C ではできない。(Pascal には関数と手続きの区別がありますが、ここでは重要ではありません。)
  • C のコードは複数のファイルに分割できるが、Pascal ではできない。

ある人の主張はこうです。「関数の中に関数が定義できて嬉しかった試しはない。しかし、コードを複数のファイルに分割できないとしたら、現実問題として大きなプログラムの開発は不可能だ。だから、C の方がいいんだ。」

僕はこの主張を真に受けてしまいました。浅はかでした。クロージャを理解していれば、違った印象を持ったことでしょう。