LoginSignup
44
45

More than 5 years have passed since last update.

JavaScriptクロージャ入門 (実行コードおまけつき)

Last updated at Posted at 2014-07-21

最初はほんとにとっつきにくいJavaScriptのクロージャへの理解の助けとなるように、いくつか例をあげてクロージャの説明をします。
各サンプルコードにはおまけでjsfiddleのリンクをつけてあるので、実際の実行結果を確認しつつ進めてください。

実際にクロージャを活用するために

これから、プログラマーが実際のコードでクロージャを利用できるようになるための説明をします。

クロージャは今やごく一般的なプログラミングのテクニックとなっていて、プログラミングの上級者だけに許されたものでもなければ、関数型プログラマーのためだけのものでもありません。

実は、核となるコンセプトさえ理解できれば、クロージャを理解することは難しくないのです。
が、いわゆる「クロージャとは?」みたいな解説を読んだだけでは理解しづらいのが厄介です。

前提として、次のJavaScriptが理解できる人を対象としています。

function sayHello(name) {
  var text = 'Hello ' + name;
  var sayAlert = function() { alert(text); }
  sayAlert();
}

sayHello('Bob'); // => "Hello Bob" と表示される

また、C言語など、他の一般的なプログラム言語を知っていればより理解しやすいと思います。

クロージャとは?

クロージャとは、一般的には以下のように定義されます。

  • クロージャとは、関数である
  • クロージャは、定義されたときに、定義されたときの環境を保持する
  • 関数の内部で関数が定義されたとき、それはクロージャとなる

先にも言ったように、ぶっちゃけこの定義を見ても全然ピンとこないです。

ので、実際の例を見てみましょう。

クロージャの例

次の関数は別の関数への参照を返しています。

function sayHello2(name) {
  var text = 'Hello ' + name;
  var sayAlert = function() { alert(text); }
  return sayAlert;
}

var say2 = sayHello2('Jane');
say2(); // => "Hello Jane" と表示される

このコードでは、sayHello2関数が戻り値として返しているのは、関数内で宣言されたsayAlertという関数への参照です。それを、say2という変数に格納して実行しています。その結果、"Hello Jane" と表示されます。

JavaScriptプログラマーであれば、この動きはごく自然に理解できることですが、もしイマイチ腑に落ちないっていうか気持ち悪い、という方は、まず次のことを理解しておく必要があります。

C言語や他の一般的なプログラム言語では、関数がリターンした後にはその関数の環境がメモリから消滅しているので、その関数のローカル変数にアクセスすることはできません。しかし、JavaScriptでは、ある関数の中でまた関数を宣言した場合、関数を呼び出してリターンした後もローカル変数がアクセス可能な状態で残り続けます。

上記の例だと、sayHello2からリターンした後に、say2という関数を呼び出していますが、呼び出した関数の中で参照しているtextという変数は、sayHello2関数のローカル変数です。これがもしC言語のように、関数がリターンした後に関数の環境が消滅しているのであれば、textへの参照なんてありえねーはずです。

ある関数のローカル変数を、関数が終了した後も、関数内で定義された関数が保持し続けることができること。

これがクロージャです。

このことがつまり、上の方のクロージャの定義で書いた以下の意味を示しています。

  • クロージャは、定義されたときに、定義されたときの環境を保持する

例えばC言語経験者であれば、関数が終了した後も、そのスタックフレームが保持される、と例えた方がわかりやすいかもしれません。スタックフレームがmallocで確保されるような感じと言ってもよいでしょう。
もちろん、これはあくまでも例えなので、技術的にそれらと等しいわけではないです。

まとめると以下になります。

  • 関数 function() { alert(text); } は、sayHello2という別の関数の内側で定義されたクロージャ
  • クロージャは、それが定義されたときの環境を保持するため、ローカル変数textを保持している

このように、JavaScriptにおいて、ある関数の中でfunctionキーワードを使うことは、すなわちクロージャを作っていることを意味します。

確認のために、ここで1つ試してみましょう。さっきのコードに続けて以下を実行してみてください。

alert(say2.toString());

匿名関数 function() { alert(text); } が表示されたと思います。そして、そこで変数textを参照していることが分かります。その匿名関数はクロージャです。ということは、変数textはクロージャによって保持された環境であり、実際に "Jane" という値が入った状態なのです。

もっとクロージャの例を見てみよう

クロージャは理解するのがとても難しいと感じるかもしれません。そこで、ただ読むだけでなく、サンプルコードの例を実際に動かしてみて確認するのが良いです。

クロージャのローカル変数は参照である

クロージャにローカル変数が保持されると書きました。では、そのクロージャの中に保持されたローカル変数は、コピーされたものなのでしょうか?

クロージャは、定義されたときの環境を「参照」として保持します。つまり、保持されたローカル変数はコピーではなく参照です。

次のサンプルコードを見てください。

function say667() {
  var num = 666;
  var sayAlert = function() { alert(num); }
  num++;
  return sayAlert;
}

var sayNumber = say667();
sayNumber(); // => 667 と表示される

sayAlert関数の中で参照される変数numは、sayAlert関数が定義された後でインクリメントされていますが、実行したときに表示されるのは、インクリメント後の値である "667" です。これより、クロージャが保持している変数は値のコピーではなく、参照であることが分かります。

同一の環境を保持するクロージャは共有される

次の例では、3つの関数をグローバルに定義しています。そして、それらすべての関数は同一の環境に対しての参照を持っていることを示しています。なぜかいうと、3つの関数はすべてsetupSomeGlobalという1つの関数の中で宣言されているからです。

function setupSomeGlobals() {
  var num = 666;
  gAlertNumber = function() { alert(num); }
  gIncreaseNumber = function() { num++; }
  gSetNumber = function(x) { num = x; }
}

クロージャとは、定義されたときの環境への参照を保持する関数でした。上記の3つの関数が保持するのは、すべて同一の環境です。(ここでは、より具体的に、3つの関数はすべて同一のローカル変数numを参照している、と言ってよいでしょう)

したがって、例えばgIncreaseNumber関数の実行後にgAlertNumberを実行すると、インクリメントされたnumの値が表示されています。

この例でもう1つ押さえておくべきことは、もしsetupSomeGlobals()を再度クリックした場合、新たにクロージャが作成されることです。ということは、グローバル変数のgAlertNumber、gIncreaseNumber、gSetNumberは、その新たなクロージャを持った新しい関数によって上書きされるということです。

JavaScriptでは、ある関数の内側で関数を宣言した場合、内側で宣言された関数は、外側の関数が呼び出されるたびに新しく作り直されます。つまり、クロージャが再生成されます。

ループの中でクロージャを定義する場合の注意

次の例で示すのは、多くの人が最初はひっかかるであろうトラップ的なやつです。
関数をループの中で定義する時には注意してね、というやつです。

function buildList(list) {
    var result = [];
    for (var i = 0; i < list.length; i++) {
        var item = 'item' + list[i];
        result.push( function() {alert(item + ' ' + list[i])} );
    }
    return result;
}

function testList() {
    var fnlist = buildList([1,2,3]);
    for (var j = 0; j < fnlist.length; j++) {
        fnlist[j]();
    }
}

これを実行してみるとどうなるでしょうか?

"item3 undefined" と3回表示されました!

なにこれ?なぜこんな結果になるのでしょうか?

前の3つのグローバル関数の例でも見たように、クロージャが保持するのは環境への参照であるため、
1つの関数内でクロージャに参照される環境は1つだけです。つまり、buildList関数が持つローカル変数へのクロージャは1つだけしか存在しません。

したがって、fnlist[j](); の行で匿名関数が呼び出されたとき、それらはすべて同一のクロージャを共有します。そのときの変数iとitemの値にはクロージャの現在の値が使われます。

実際に参照するときにはループは完了しているので、iの値は3、itemの値は "item3" となっているため、この実行結果となるのです。

クロージャの振る舞いを正しく理解しておけば納得の結果なのですが、直感的な感覚に反しているので、この振る舞いには注意しておきましょう。

クロージャが保持する環境は関数内全体

次の例は、「クロージャは、その外側の関数を抜ける前に宣言された関数内の変数なら、どこで宣言されたものであっても保持している」ということを示すためのものです。

function sayAlice() {
  var sayAlert = function() { alert(alice); }
  var alice = 'Hello Alice';
  return sayAlert;
}

変数aliceが宣言されている箇所が、匿名関数の宣言より後であることに注目してください。

匿名関数の宣言の方が先にされている…にも関わらず、その関数が呼び出されたときにaliceにアクセスできています。これは、クロージャは、それが宣言される場所に関わらず、関数内全体の環境を保持する性質を持つためです。

クロージャは関数呼び出しごとに生成される

最後の例です。

  • クロージャは関数定義ごとに1つだけ存在するのではなく、 関数呼び出しごとに生成される

ことを示します。

function newClosure(someNum, someRef) {
  var num = someNum;
  var anArray = [1,2,3];
  var ref = someRef;
  return function(x) {
      num += x;
      anArray.push(num);
      alert('num: ' + num + 
          '\nanArray ' + anArray.toString() + 
          '\nref.someVar ' + ref.someVar);
    }
}

function createClosures() {
    closure1 = newClosure(40, {someVar : 'closure 1'});
    closure2 = newClosure(1000, {someVar : 'closure 2'});
}

これまで、同一の関数内ではクロージャが共有される、という言い方をしてきましたが、この例から分かるように、クロージャの生成される単位は「関数呼び出しごと」です。

先の「同一の環境を保持するクロージャは共有される」という例では、グローバルにクロージャが定義されていたために、関数呼び出しごとにクロージャが上書きされていましたが、関数内ローカルのスコープで定義されたクロージャは関数呼び出しごとに生成されるので、同一関数で定義されていても呼び出しごとに別物になります。

クロージャのこの性質を利用して、状態を持つオブジェクトを定義することがよくあります。つまり、クラスベースのオブジェクト指向でいうところのクラス定義のようにクロージャを利用することがあります。

まとめ

この投稿は、以下の記事に大きな影響を受けています。特にサンプルコードはほぼすべて引用です。

http://web.archive.org/web/20080209105120/http://blog.morrisjohns.com/javascript_closures_for_dummies
※元記事は消えた・・・?

オリジナルでは関数ポインタを引き合いに出している箇所など、C言語経験者向けの内容になっている場所はできる限りなくしましたが、スタックフレームなどの例えはそのまま真似しています。

44
45
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
44
45