JavaScriptのクロージャを1つずつ理解する

クロージャとは

クロージャは、独立した (自由な) 変数を参照する関数です。言い換えるとクロージャ内で定義された関数は、自身が作成された環境を '覚えています'。

クロージャ - JavaScript | MDN

このままだとなんのこっちゃ良く分からないので,ステップバイステップで進めていきます.

クロージャの理解に必要なパーツ

即時関数

名前がなく,すぐに実行される関数を即時関数と言います.
フォーマットは以下の通りです.

(function() { 
  // 何らかの処理
}());

即時関数を用いることで,

  • ちょっとした処理をまとめて実行できる
  • グローバルスコープを汚染することなく変数を定義できる

といった利点があります.

// 1から10までの総和を即時関数で求める
// 変数sumはグローバルスコープを汚染しない
var sum1to10 = (function() {
  var sum = 0;
  for(var i = 1; i <= 10; i++){
    sum += i;
  }
  return sum
}());

console.log(sum1to10); // 55

実行コンテキスト

以前の記事で,実行コンテキストについて書きました.
szarny.hatenablog.com

関数はそれぞれが実行コンテキストを持ち,その中でローカル変数や関数内関数を保持し、必要に応じて参照します.
但し,ある関数内で参照されているオブジェクトがその関数の実行コンテキストに存在しなかった場合,外側の方向へそのオブジェクトを探しに行きます.
いわゆる,スコープチェーンです.
f:id:Szarny:20170930143715p:plain:w300

ガーベジコレクション

JavaScriptは,いらなくなったオブジェクト,つまりどこからも参照されていないオブジェクトを自動的にメモリ上から排除する機能を持ちます.
これがガーベジコレクションです.

例えば,以下のようなプログラム

function sayHello(){
  var hello = "Hello!";
  console.log(hello);
}

sayHello(); // Hello!

関数sayHello()を呼び出すと,実行コンテキストが生成され,ローカル変数helloが設定されます.
そして,console.log(hello);によって,Hello!が出力されます.

これら実行が終わった後,関数sayHelloの実行コンテキストは誰からも参照されていない状態になります.
よって,ローカル変数helloは,関数の実行が終了した時点(もしくは少し後)でガーベジコレクションによって破棄されます.

いよいよクロージャ

何ができるのか

クロージャの定義を再掲します.

クロージャは、独立した (自由な) 変数を参照する関数です。言い換えるとクロージャ内で定義された関数は、自身が作成された環境を '覚えています'。

クロージャ - JavaScript | MDN

結論から述べると,クロージャを用いることで,ガーベジコレクションに実行コンテキストが排除されるのを食い止めることができます.
つまり,関数内で定義したローカル変数の値を,永続的に残すことができるようになります.

問題ありのプログラム

例えば,アクセスカウントを行うプログラムを作成したいとします.
すぐに思いつくのは,以下のようなプログラムです.

var access_counter = function(){
  var access_count = 0;
  var accessor = function(){
    access_count += 1;
    console.log("You've accessed " + access_count + " times.");
  }
  
  accessor();
}

for(var i = 1; i <= 3; i++){
  access_counter();
}
// "You've accessed 1 times."
// "You've accessed 1 times."
// "You've accessed 1 times."

しかし,うまくいきません.

なぜなら,access_counterに格納されている関数の実行コンテキストは,実行された後誰からも参照されずにガーベジコレクションの対象になるからです.

つまり,実行のたびに,ローカル変数access_countが生成されては消え,生成されては消えを繰り返しているわけです.

f:id:Szarny:20170930181750p:plain:w500

クロージャで実現

そこで,クロージャを用います.
具体的には,access_counterに関数オブジェクトを代入するのではなく,関数を即時実行させ,関数内関数のaccessorを返すようにします.

var access_counter = (function(){
  var access_count = 0;
  var accessor = function(){
    access_count += 1;
    console.log("You've accessed " + access_count + " times.");
  }
  
  return accessor;
})();

for(var i = 1; i <= 3; i++){
  access_counter();
}
// "You've accessed 1 times."
// "You've accessed 2 times."
// "You've accessed 3 times."

すると,期待していた結果を返すようになります.

これは,access_counteraccessorへの参照を保持することにより,外側の関数の実行コンテキストが破棄されなくなったためです.

f:id:Szarny:20170930182946p:plain:w500

accessorへの参照が存在している」ということは,スコープチェーンによって外側の関数の実行コンテキストへの参照が行われる可能性があることを暗示しています.

これにより,JavaScriptエンジンは,外側の関数の実行コンテキストが将来的に利用される可能性を考慮して,ガーベジコレクションを行わなくなるのです.

そのため,外側の関数の実行コンテキストにあるローカル変数access_countは排除されることなく,スコープチェーンによって,関数内関数accessorから参照され続けます.

よって,クロージャ内の関数(accessor)は,自身が作成された時の環境,つまりクロージャのローカル変数を覚えておくことができるのです.