クロージャとレキシカルスコープ

自分なりのまとめです。

【他、参考となるサイト】
http://d.hatena.ne.jp/keyword/%a5%af%a5%ed%a1%bc%a5%b8%a5%e3
http://www.atmarkit.co.jp/fdotnet/ajaxjs/ajaxjs03/ajaxjs03_03.html
http://www.atmarkit.co.jp/fdotnet/ajaxjs/ajaxjs03/ajaxjs03_04.html

内部関数

関数内で更に関数を定義することができます。

function test(test_str){
  function sub(sub_str){
    alert(sub_str);
  }
  sub("TEST:" + test_str);
}
test("!!!"); //「TEST:!!!」と表示される。
//←ここでsub("!!!");とはできない。

関数subをこのように定義すれば、親関数test内でしか使わないことがはっきりしますし、名前の競合も防ぎやすくなります。

クロージャ

内部関数とクロージャはほぼ同義なんですが、ここでは分けてみました。親関数の外に参照が渡される内部関数をクロージャと呼ぶことにします。→すみません、以降、内部関数もすべてクロージャと記述させてください。

「親関数の外に参照が渡される」とは、具体的には

  • 返却値
  • 関数呼び出しの引数
  • (グローバル等の)親スコープ変数への格納
  • 既存オブジェクトのメンバとして登録*1

等になります。

var g_sub;
function other(a_sub){
  a_sub("ARG"); //「TEST:ARG」と表示される。
}
function test(){
  function sub(sub_str){
    alert("TEST:" + sub_str);
  }
  other(sub); //引数
  g_sub = sub; //グローバル変数へ格納
  return sub; //返却
}
var r_sub = test();
r_sub("RET"); //「TEST:RET」と表示される。
g_sub("GLB"); //「TEST:GLB」と表示される。

注目してほしいのは、関数testの実行終了後も、関数subが有効であることです。

レキシカルスコープ

親関数の変数・仮引数を使用可能

クロージャは、親関数の変数・仮引数を使用することができます。

var g_sub;
function other(a_sub){
  a_sub("ARG"); //「TEST:0:ARG」と表示される。
}
function test(test_str){
  var test_count = 0;
  function sub(sub_str){
    alert(test_str + ":" + test_count + ":" + sub_str);
  }
  other(sub); //引数
  g_sub = sub; //グローバル変数へ格納
  return sub; //返却
}
var r_sub = test("TEST");
r_sub("RET"); //「TEST:0:RET」と表示される。
g_sub("GLB"); //「TEST:0:GLB」と表示される。

ここで注目してほしいのは、関数testの実行終了後も、その変数・仮引数が有効であるということです。このスコープ特性をレキシカルスコープといいます。外に渡されたクロージャへの参照が保持される間、その親関数の変数・仮引数も保持されます。

親関数の変数・仮引数は、その実行毎に保持

上記の例で、関数subにtest_countをカウントアップする処理を追加してみます。

var g_sub;
function other(a_sub){
  a_sub("ARG"); //「TEST:1:ARG」と表示される。
}
function test(test_str){
  var test_count = 0;
  function sub(sub_str){
    test_count++; //カウントアップ
    alert(test_str + ":" + test_count + ":" + sub_str);
  }
  other(sub); //引数
  g_sub = sub; //グローバル変数へ格納
  return sub; //返却
}
var r_sub = test("TEST");
r_sub("RET"); //「TEST:2:RET」と表示される。
g_sub("GLB"); //「TEST:3:GLB」と表示される。

カウントアップされるのが分かると思います。つまり、クロージャ間で親関数の変数・仮引数は共有されているのです。

ここで、test関数を2回実行してみます。

var g_sub;
function other(a_sub){
  a_sub("ARG"); 
}
function test(test_str){
  var test_count = 0;
  function sub(sub_str){
    test_count++; //カウントアップ
    alert(test_str + ":" + test_count + ":" + sub_str);
  }
  other(sub); //引数
  g_sub = sub; //グローバル変数へ格納
  return sub; //返却
}
var r_sub = test("TEST");
r_sub("RET"); 
g_sub("GLB"); 
//もう1回
var r_sub_m = test("TEST_M");
r_sub_m("RET"); 
g_sub("GLB"); //g_subを3回実行
g_sub("GLB"); 
g_sub("GLB"); 
//初回に返却されたクロージャ
r_sub("RET");

実行結果は以下です。

以下が順番に表示される。
TEST:1:ARG
TEST:2:RET
TEST:3:GLB
TEST_M:1:ARG
TEST_M:2:RET
TEST_M:3:GLB
TEST_M:4:GLB
TEST_M:5:GLB
TEST:4:RET

実行一回目のクロージャと、実行二回目のクロージャで変数が共有されていないことが分かります。つまり、親関数の実行毎に変数・仮引数が保持されているということです。

クロージャはグローバル(windowオブジェクトのメンバ)として実行される

クロージャをそのまま実行すると、グローバル(windowオブジェクトのメンバ)として実行されます。このため、親関数内のthisとクロージャ内のthisは意味が異なります。

<button id="button">TEST</button>
<script>
var button = document.getElementById("button");
function test(){
  alert(this.id); //「button」と表示される。
  alert((this == button)); //「true」と表示される。
  function sub(){
    alert(this.id); //「undefined」と表示される。
    alert((this == window)); //「true」と表示される。
  }
  sub();
} 
button.onclick = test;
</script>

要素のonclickプロパティに登録された関数は、イベント発生時、要素のメンバとして実行されます。(thisが要素を意味する。) クロージャで親関数のthisを参照したい場合は、以下のように親関数内でthisを変数に格納して参照します。

<button id="button">TEST</button>
<script>
var button = document.getElementById("button");
function test(){
  alert(this.id); //「button」と表示される。
  alert((this == button)); //「true」と表示される。
  var _this = this; //変数にthisを格納。
  function sub(){
    alert(_this.id); //「button」と表示される。
    alert((_this == window)); //「false」と表示される。
  }
  sub();
} 
button.onclick = test;
</script>

または、クロージャをapply/callメソッドを使用して呼び出すことでも可能です。

<button id="button">TEST</button>
<script>
var button = document.getElementById("button");
function test(){
  alert(this.id); //「button」と表示される。
  alert((this == button)); //「true」と表示される。
  function sub(){
    alert(this.id); //「button」と表示される。
    alert((this == window)); //「false」と表示される。
  }
  sub.apply(this);
} 
button.onclick = test;
</script>

呼び出しにオブジェクトを必要としない分、前者のほうが汎用性は高いと思います。

クロージャは他のコードより優先して生成される(※例外あり、後述)

問題です。以下のalertは何が表示されるでしょう。

function test(){
  var count = 0;
  count++;
  function sub(){
    alert(count); //???
  }
  count++;
  return sub;
} 
var retFunc = test();
retFunc();

親関数の変数、およびそれを使った処理が、クロージャの前にあるか後ろにあるかは関係ありません。問題はクロージャ実行時にどうなっているかです。上記例の場合は、return後の状態になります。なので、答えは「2」です。

・・・というか、クロージャの親関数内での位置には意味がありません。コメントいただいて気が付いたのですが

JSではfunctionやvarによる宣言部分(変数の初期化代入は宣言に含まれません)は、スコープ内で他のコードよりも先に処理される

のですね。

function test(){
  var str = "TEST";
  return sub;
  function sub(){
    alert(str);
  }
} 
var retFunc = test();
retFunc(); //「TEST」と表示される
例外:クロージャ(関数)が式に組み込まれている場合

ただし、クロージャが式に組み込まれている場合は少し話が違います。以下の実行結果は?

function test(){
  var str = "TEST";
  return sub; 
  var wrap = function sub(){
    alert(str);
  };
} 
var retFunc = test();
retFunc();

正解は、「ブラウザによって異なる」です・・・。

  • IE:「TEST」と表示される。
  • Firefox:3行目でエラー。(sub is not defined)
  • Opera:3行目でエラー。(Reference to undefined variable: sub)

あとで気が付いたのですが、FirefoxOperaでは、subが有効なスコープはその関数の中だけになるようです。IEについては、あらかじめ生成した上、実行時にも生成しているような気がします。

*2

例1

過去に回答した質問question:1180098481を題材にさせていただきます。

問題です。次のコードの最終2行実行時、何が表示されるでしょう。

var a = new Array();
function f(){
  for(var i = 0;i < 10;i++){
    a[i] = function(){
      window.alert(i);
    };
  }
}
f();
a[0](); //???
a[9](); //???

「0」と「9」、と思えるかもしれません。実際、質問者の方もそう思われたのです。しかし、すでに挙げたクロージャの性質、

  • クロージャの親関数の変数・仮引数は、その実行毎に保持される

という点や、今までの例を踏まえれば、2回とも「10」が表示される、ということがご理解いただけると思います。

ここでさらに問題。配列aに格納されているクロージャは、それぞれ同じものでしょうか、それとも別のものでしょうか。

var a = new Array();
function f(){
  for(var i = 0;i < 10;i++){
    a[i] = function(){
      window.alert(i);
    };
  }
}
f();
alert((a[0] == a[9])); // true or false ?

正解はfalseです。別なんですね。前述したように、式に組み込まれているクロージャは、その実行時に生成されているのです。これはIEでもFirefoxOperaでも同じです。ただ、使用している変数が同じなので、同じ挙動になるのです。

さて、それぞれ違う値を表示する関数を配列aに登録するにはどうしたらよいのでしょう。

var a = new Array();
function f(){
  function make(j){
    return function(){
      window.alert(j);
    };
  }
  for(var i = 0; i < 10; i++){
    a[i] = make(i);
  }
}
f();
a[0]();
a[9]();

上記は私の回答です。ちょっとややこしいですね^^; クロージャを返すクロージャmakeを作成して、for文で実行しています。こうすることで、make実行毎に仮引数jが保持されるので、配列aの関数は、それぞれ違う挙動になるのです。

例2

クロージャの威力がよく発揮されるシーンとして、タイマーイベントや要素のイベントへの関数登録があります。

<html>
<head>
<script>
function test(id){
  var elem = document.getElementById(id);
  var count = 0;
  elem.onclick = start;
  var tid = null;
  
  function start(){
    tid = setInterval(countup, 1000);
    elem.onclick = stop;
  }
  function countup(){
    elem.innerHTML = count;
    count++;
  }
  function stop(){
    clearInterval(tid);
    elem.onclick = start;
  }
}

window.onload = function(){
  test("div1");
  test("div2");
}
</script>
</head>
<body>
それぞれクリックするとスタート←→ストップが切り替わります。
<div id="div1" style="border:solid blue 2px;background-color:#ccccff">クリックするとスタート</div>
<div id="div2" style="border:solid red 2px;background-color:#ffcccc">クリックするとスタート</div>
</body>
</html>

青い枠のDIVと赤い枠のDIVがあります。それぞれクリックすると、1秒ごとに数値をカウントアップして表示します。もう一度クリックすると停止し、更にクリックすると再開します。それぞれの数値は独立しています。

この機能を、関数testのみで実装しています。タイマー識別子やカウンタをグローバルエリアに持つ必要はありません。また、表示要素が増えたとしても、その分関数testを呼び出せばいいだけです。関数testに改変は必要ありません。こんなことができるのも、レキシカルスコープの特性があるからです。

おわりに

レキシカルスコープは、「prototype.js解読」のときにEnumerableクラスのeachメソッドで出くわして、散々悩みました^^; 経験言語(CやJava)にはない話だったので・・・。でも、いちど理解できると、JavaScriptが非常に効率よく組めるようになり、楽しくなった覚えがあります。この記事が、みなさんのJavaScript効率化の一助になれば幸いです。

・・・また、間違いがありましたらコメント等でご指摘くださいますよう、お願いいたします。。。m_ _m

*1:7/23 追記しました。以降の例では取り上げてません^^;

*2:以前、グローバルエリアで関数定義を後にしたら参照できないという、間違った内容をここに書いて、コメントで指摘いただきました。