クロージャは、独立した (自由な) 変数を参照する関数です。言い換えるとクロージャ内で定義された関数は、自身が作成された環境を '覚えています'。
レキシカルスコープ
次のような関数を考えます。
init()
関数はローカル変数 name
を作成し、それから関数 displayName()
を定義しています。displayName()
は init()
の中で定義されている内部関数で、その関数本体の内部でしか利用できません。displayName()
自体はローカル変数を持っていませんが、外側の関数で宣言された変数にアクセスしていますので、親関数で宣言された変数 name
を使用できます。
コードを実行して、動作を確認してください。これはレキシカルスコープを示す例です。JavaScript では、変数のスコープはソースコード内の位置によって決定され、入れ子にされた関数は外側のスコープで宣言された変数にアクセスすることができます。
Closure
今度は次のような例を考えます。
function makeFunc() { var name = "Mozilla"; function displayName() { alert(name); } return displayName; } var myFunc = makeFunc(); myFunc();
このコードを実行すると、前の init()
の例と全く同じように文字列 "Mozilla" が JavaScript の警告ボックスに表示されます。前の例とは異なる、興味深い点は、内部関数 displayName()
がそれが実行される前に外部関数から返されているという事です。
このコードが動作するということは直感的に理解できないかもしれません。通常は、関数内部のローカル変数はその関数が実行されている間だけ存在します。一旦 makeFunc()
の実行が完了したら、name 変数はもう必要とされなくなると考えた方が筋は通っています。ただこのコードが期待したとおりに動くという事は、これは明らかに事実と異なります。
この謎に対する解答は、myFunc
がクロージャになったということです。クロージャは関数とその関数が作られた環境という 2 つのものが一体となった特殊なオブジェクトです。この環境は、クロージャが作られた時点でスコープ内部にあったあらゆる変数によって構成されています。この場合、myFunc
はそれが作られた時に存在していた displayName
関数と 文字列 "Mozilla" を取り込んだクロージャです。
ここにもう少し面白い例があります。makeAdder
関数です。
function makeAdder(x) { return function(y) { return x + y; }; } var add5 = makeAdder(5); var add10 = makeAdder(10); console.log(add5(2)); // 7 と表示される console.log(add10(2)); // 12 と表示される
この例では、1 つの引数 x
を取り、新しい関数を返す makeAdder(x)
関数を定義しています。返される関数は 1 つの引数 y
を取り、x
と y
の和を返します。
要するに、makeAdder
は関数ファクトリです。これは与えられた引数に特定の値を足す関数を作ります。上の例では関数ファクトリを使って 2 つの新しい関数を作成しています。 1 つは引数に 5 を加えるもので、もう 1 つは 10 を加えるものです。
add5
と add10
は両方ともクロージャです。両者は同じ関数本体の定義を共有していますが、保有している環境は異なります。add5
の環境では x
は 5 で、add10
の環境では x
は 10 です。
実用的なクロージャ
理論はこれぐらいにしておくとして、クロージャは実際の役に立つのでしょうか? クロージャの実用上の意義を考えてみましょう。クロージャを使うと、データ (環境) をそれを操作する関数と結びつける事が出来ます。これはオブジェクトを使ってデータ (オブジェクトのプロパティ) を 1 つかそれ以上のメソッドと結びつける事が出来るオブジェクト指向プログラミングと明らかに類似しています。
したがって、メソッドを 1 つだけ持つオブジェクトを使いたくなるような状況ならば、どんな時でもクロージャを使う事ができます。
web ではこのような状況はよくあります。私たちが書く JavaScript のコードは大半がイベントベースです。つまり、ある動作を定義し、それを click や keypress といったユーザーによって引き起こされるイベントに取り付けます。私たちのコードの多くはコールバック、すなわちイベントに反応して実行される単独の関数として取り付けられます。
実例を挙げましょう。あるページにそのページのテキストの大きさを調整するためのボタンを追加しようとしているとします。1 つの方法として、まず body 要素の font-size をピクセル数で指定して、そのページ内の (見出しなどの) 他の要素のサイズを相対単位 em で設定します。
body { font-family: Helvetica, Arial, sans-serif; font-size: 12px; } h1 { font-size: 1.5em; } h2 { font-size: 1.2em; }
これから作る対話式のテキストサイズ調整ボタンは、body 要素の font-size プロパティを変更し、その変更は相対単位によってページ上のほかの要素にも適用されます。
JavaScript のコード:
function makeSizer(size) { return function() { document.body.style.fontSize = size + 'px'; }; } var size12 = makeSizer(12); var size14 = makeSizer(14); var size16 = makeSizer(16);
size12
、size14
、size16
はそれぞれ body のテキストサイズを 12、14、16 px に変更する関数になっています。これらは以下のようにしてボタン (この場合はリンク) に取り付けられます。
document.getElementById('size-12').onclick = size12; document.getElementById('size-14').onclick = size14; document.getElementById('size-16').onclick = size16;
<a href="#" id="size-12">12</a> <a href="#" id="size-14">14</a> <a href="#" id="size-16">16</a>
クロージャでプライベートメソッドを模倣する
Java などの言語ではプライベートなメソッドを宣言することが出来ます。これは同じクラス内にあるほかのメソッドからのみ呼び出せるメソッドのことです。
JavaScript にはこういった機能は組み込まれていませんが、クロージャを使うとプライベートメソッドを模倣する事ができます。プライベートメソッドはコードへのアクセスを制限するのに役立つだけではなく、コードのパブリックインターフェースが不要なメソッドでいっぱいになるのを防ぐため、グローバル名前空間を管理するのに非常に有効です。
>モジュールパターンとしても知られる、クロージャを使って、プライベートな関数と変数にアクセスできるパブリック関数を定義するにはこのようにします。
var counter = (function() { var privateCounter = 0; function changeBy(val) { privateCounter += val; } return { increment: function() { changeBy(1); }, decrement: function() { changeBy(-1); }, value: function() { return privateCounter; } }; })(); console.log(counter.value()); /* 0 と表示される */ counter.increment(); counter.increment(); console.log(counter.value()); /* 2 と表示される */ counter.decrement(); console.log(counter.value()); /* 1 と表示される */
ここでは色々な事が行われています。前の例ではクロージャがそれぞれ独自の環境を持っていましたが、この例では環境が 1 つだけ作成され、その環境は counter.increment
、counter.decrement
、counter.value
という 3 つの関数によって共有されています。
この共有環境は、定義されるとすぐに実行される無名関数の本文で作成されています。この環境は変数 privateCounter
と関数 changeBy
という 2 つのプライベートアイテムを含んでいます。これらはどちらも無名関数の外側からは直接アクセス出来ません。その代わり、この無名ラッパ関数から返される 3 つのパブリック関数からはアクセスできます。
これら 3 つのパブリック関数は同じ環境を共有するクロージャです。JavaScript のレキシカルスコーピングにより、これらの関数はそれぞれが変数 privateCounter
と関数 changeBy
にアクセスできます。
私たちはカウンタを作成する無名関数を定義して、その後すぐに関数を呼び出して結果を変数 counter
へ代入していることに気づいているでしょう。この関数を別の変数 makeCounter
に保存して、複数のカウンタを作成するために使用することもできます。
var makeCounter = function() { var privateCounter = 0; function changeBy(val) { privateCounter += val; } return { increment: function() { changeBy(1); }, decrement: function() { changeBy(-1); }, value: function() { return privateCounter; } } }; var counter1 = makeCounter(); var counter2 = makeCounter(); alert(counter1.value()); /* 0 と表示される */ counter1.increment(); counter1.increment(); alert(counter1.value()); /* 2 と表示される */ counter1.decrement(); alert(counter1.value()); /* 1 と表示される */ alert(counter2.value()); /* 0 と表示される */
どのようにしてそれぞれのカウンタを、他のカウンタとは独立して管理しているかに注意してください。makeCounter()
を呼び出している間の環境は、毎回異なります。またクロージャ変数 privateCounter
は、毎回異なるインスタンスを持ちます。
このようにしてクロージャを使うと、普通はオブジェクト指向プログラミングに付き物のいくつかの利点、具体的にはデータの隠蔽やカプセル化が利用できるようになります。
よくある間違い: ループ内でクロージャを作成する
ECMAScript 6 で let
キーワードが導入される前までは、ループの内部でクロージャが作成された時にある問題がよく起こっていました。次のような例を考えます:
<p id="help">ここにヘルプが表示されます</p> <p>Eメール: <input type="text" id="email" name="email"></p> <p>名前: <input type="text" id="name" name="name"></p> <p>年齢: <input type="text" id="age" name="age"></p>
function showHelp(help) { document.getElementById('help').innerHTML = help; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'あなたのEメールアドレス'}, {'id': 'name', 'help': 'あなたのフルネーム'}, {'id': 'age', 'help': 'あなたの年齢 (17 歳以上)'} ]; for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } } } setupHelp();
配列 helpText
は 3 つのヘルプを定義しており、それぞれがドキュメント内の入力フィールドの ID と関連付けられています。ループがこれらの定義を巡回して、それぞれの入力フィールドの onfocus イベントをそれに関連付けられたヘルプを表示するメソッドと結び付けています。
このコードを実行してみると、期待したとおりには動かないのが判ります。どのフィールドにフォーカスしても、表示されるのは年齢についてのメッセージです。
こうなる理由は、onfocus
に代入された関数がクロージャだからです。このクロージャは、関数定義と、setupHelp
関数のスコープから捕捉された環境から成っています。クロージャは 3 つ作られましたが、これらはみな 1 つの同じ環境を共有しています。onfocus
コールバックが実行される時にはループはすべて終了しており、変数 item (3 つのクロージャ全てに共有されている) は helpText
リストの最後の項目を示したままにされています。
こういった場合の解決策の 1 つとして、より多くのクロージャを使う方法があります。具体的には、前に述べたような関数ファクトリを使います。
function showHelp(help) { document.getElementById('help').innerHTML = help; } function makeHelpCallback(help) { return function() { showHelp(help); }; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'あなたのEメールアドレス'}, {'id': 'name', 'help': 'あなたのフルネーム'}, {'id': 'age', 'help': 'あなたの年齢 (17 歳以上)'} ]; for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onfocus = makeHelpCallback(item.help); } } setupHelp();
これは期待通り動きます。全てのコールバックが 1 つの環境を共有するのではなく、makeHelpCallback
関数がそれぞれに対して新しい環境を作っており、そこでは help
が配列 helpText
の対応する文字列を参照しています。
パフォーマンスへの配慮
あるタスクを実行する時、クロージャが必要とされていないのにいたずらに関数を他の関数の中に作成するのは、スクリプトのパフォーマンスに悪影響を及ぼすのであまり賢いやり方ではありません。
例えば、新しくオブジェクト/クラスを作成する時、一般的にメソッドはオブジェクトのコンストラクタの中で定義するのではなく、オブジェクトのプロトタイプに結びつけるべきです。コンストラクタの中で定義してしまうと、コンストラクタが呼び出されるたびに (つまりオブジェクトが作成されるたびに) メソッドが再代入されてしまうことになるからです。
次の実践的ではない例証のためのケースを考えます。
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); this.getName = function() { return this.name; }; this.getMessage = function() { return this.message; }; }
上のコードではクロージャを使う事によって得るものが何もないので、再構成するべきです。
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); } MyObject.prototype = { getName: function() { return this.name; }, getMessage: function() { return this.message; } };
ただし、プロトタイプの再定義は推奨されません。以下の例のように既存のプロトタイプに追加するのがより好ましい方法です。
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); } MyObject.prototype.getName = function() { return this.name; }; MyObject.prototype.getMessage = function() { return this.message; };
より明快な形で、上のコードと同じ結果になるよう記述することもできます。
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); } (function() { this.getName = function() { return this.name; }; this.getMessage = function() { return this.message; }; }).call(MyObject.prototype);
上の 2 つの例では、プロトタイプが継承されて全てのオブジェクトによって共有されるため、オブジェクトが作成されるたびにメソッドが定義されずに済みます。 詳しくはオブジェクトモデルの詳細 を参照して下さい。