JavaScriptをしっかり勉強 vol.4 関数スコープとクロージャ

プログラム
date 2014.05.18 tag JavaScript

JavaScriptを勉強したことを書いていくエントリーvol.4です。今回は関数のスコープとクロージャについて書いていきます。

関数のスコープ

クロージャを理解するために関数のスコープについて再確認します。

まず関数内「{}」で定義した変数はその中でのみ有効で、この変数を参照できる範囲をローカルスコープと言います。そして関数の外からその変数を参照することはできません。またグローバルスコープで定義した変数は、関数の中で定義していなくても参照することができます。

スコープに関して一言でまとめると「中から外は覗けるが、外から中は覗けない」というものです。

JSグローバルスコープとローカルスコープの参照範囲を確認
//グローバルスコープでの変数定義
var global_scope = 'global_scope';

function testScope() {
  var local_scope = 'local_scope';

  //関数内で定義した変数は参照できる
  alert(local_scope);

  //グローバルスコープの変数も参照できる
  alert(global_scope);
}

//関数の外ではローカルスコープを参照できない (ReferenceError)
alert(local_scope);

//グローバルスコープはもちろん参照できる
alert(global_scope);

JavaScriptの関数スコープ

ここからJavaScriptに特化した話です。

JavaScriptでは関数の中に関数を含むことができ、それに対するスコープが存在します。ただし先ほどのように「スコープの中から外を覗ける」ということには変わりません。

JS関数スコープの参照範囲を確認
//入れ子状態を持つ関数を定義
function parentFunction() {
  var parent_text = 'parent_text';

  //子関数の実行
  function childFunction() {
    var child_text = 'child_text';
    result.innerHTML = parent_text;
  }

  //子関数の実行
  childFunction();
}

//実行すると親のスコープを参照できている
parentFunction();

//グローバルスコープで変数の範囲確認 ※ReferenceErrorとなる
result.innerHTML += parent_text; result.innerHTML += child_text;

クロージャ

ここまでの説明で入れ子状態の関数の中から、外側の関数のスコープを参照することを確認できました。

では関数の戻り値に入れ子の関数を指定するとどうでしょうか。

結果的に戻り値を格納した変数から、元の関数の中にある変数を参照することができます。これは本来のスコープの範囲を超えて変数を参照できている状態です。 概念的には分かりにくいので、まずはサンプルを掲載してからクロージャの仕組みについて解説していきます。

JSクロージャのサンプル : 関数内のローカル変数を代理で参照
//出力用フィールの取得
var result = document.getElementById('result');

//クロージャの作成
function parentFunction() {
  var parent_int = 0;

  function childFunction() {
    result.innerHTML += parent_int++;
  }

  return childFunction;
}

//入れ子関数childFunction()を変数「echo_count」に格納
var echo_count = parentFunction();

//0を表示
echo_count();

//1を表示
echo_count();

//2を表示
echo_count();

クロージャを使うことによって、カウント用のグローバル変数を用いること無くカウンターを実装することができました。

また関数スコープの特性上、外側のスコープからクロージャ内のスコープを参照、更新することができませんので変数を安心して使うことができます。

例えばカウント変数「i」をグローバルスコープで定義している中、for文でカウント変数「i」を定義するとどうでしょうか。同じ変数名同士で衝突が起きて意図しないバグを生んでしまうかもしれません。 クロージャではオブジェクト指向の機能で言うカプセル化、隠蔽に当たるものを実装することが可能です。

JSグローバル変数のコンフリクト
//カウント変数
var i = 0;

//カウント値を2まで進める
i++;
i++;
i++;

for (i = 0; i < 10; i++) { }

//forで使われた変数「i」が上書きされ、出力されるのは10
result.innerHTML = i;

クロージャの仕組み

クロージャは見えない参照が存在している状態です。

通常、関数呼び出しがreturnで終了すると中に保持していた変数などはどこからも参照されることが無くなり、メモリ上から破棄されます。(この仕組みのことをガーベージコレクションと言います。) 先ほどのクロージャのサンプルコードではparentFunctionをreturnしても、変数「echo_count」を仲介してparentFunctionへの参照を持ち続け、内部の変数を参照することができます。

js_closure

クロージャのメリット/デメリット

クロージャのメリットとしてはグローバルスコープでの変数名衝突を可能な限り防ぎ、外部からの参照制約を持つことができる点です。 逆にデメリットとしては関数への参照を常に持ち続けることによってメモリ領域が解放されず、大規模な用途で多用するとパフォーマンスの懸念がある点です。

最後にメリットとしてあげた点のサンプルコードを書いてみます。

JSカウンタの違い : グローバル変数とクロージャ
//出力用フィールドの取得
var button_global = document.getElementById('button_global');
var button_closure = document.getElementById('button_closure');

//グローバル変数を使ったカウンター
var global_count = 0;
button_global.onclick = function() {
  global_count++;
  alert(global_count);
}

//衝突により上書きが生じる
global_count = 100;

//クロージャを使ったカウンター
button_closure.onclick = (function() {
  var closure_count = 0;

  return function() {
    closure_count++;
    alert(closure_count);
  }
})();

//クロージャ内の変数にはアクセスできない
closure_count = 100;

グローバル変数「global_count」は衝突が発生し、値が上書きされてしまいます。しかしクロージャでは衝突は発生せず、内部のカウントには影響ありません。(グローバルスコープで定義した変数はグローバル変数として定義されます)