JavaScript 超入門(10)クロージャ


Javascriptを学んでみようという人をターゲットに、JavaScriptの基本を解説していきたいと思います。

他の言語(特にJava等のオブジェクト指向言語)は少しでもかじったことがあるけど、JavaScriptはまだ・・・って人を想定しています。

今回はクロージャについて解説します。
他の言語(Java等)からくるとなかなか慣れない概念の一つですが、
クロージャの理解はJavaScriptの理解を大きく進めます。頑張りどころです。

クロージャ

クロージャとは

クロージャ(クロージャー、英語: closure)、関数閉包はプログラミング言語における関数オブジェクトの一種。いくつかの言語ではラムダ式や無名関数で実現している。引数以外の変数を実行時の環境ではなく、自身が定義された環境(静的スコープ)において解決することを特徴とする。Wikipedia

なるほど、わかりませんね。

他の言い方をすると「ローカル変数への参照を持った、関数内で定義された関数」なのですが、まあピンとこないですね。
何はともあれクロージャの例を見てみることにします。
次はクロージャの説明でよく用いられる例です。

function enclosure(){
  var counter = 0;

  return function() {
    return counter++;
  }
}
var myFunc = enclosure();

console.log(myFunc()); //0
console.log(myFunc()); //1
console.log(myFunc()); //2

関数enclosureは匿名関数を返しています。
そしてその関数を実行するごとにcounterの値がインクリメントされているのがわかります。

enclosureのローカル変数はenclosureの処理が完了したタイミングで破棄されそうなものですが、
匿名関数は変数counterへの参照を保持し続けています。これがクロージャです。
またクロージャを包含している関数をエンクロージャ、参照されている変数(counter)をレキシカル変数と呼びます。
最初のうちは不思議な感じがするかもしれません。

ではenclosureを複数回呼び出した場合はどうなるでしょうか。

function enclosure(){
  var counter = 0;

  return function closure() {
    return counter++;
  }
}
var myFunc = enclosure();
var myFunc2 = enclosure();

console.log(myFunc()); //0
console.log(myFunc()); //1
console.log(myFunc()); //2
console.log(myFunc2()); //0
console.log(myFunc2()); //1
console.log(myFunc2()); //2

myFuncとmyFunc2におさめられている関数はそれぞれ異なるcounterを参照していることがわかります。
このように「関数の振る舞いは評価(実行)時ではなく、定義時の環境で決まる」という性質を表しているのがクロージャなのです。
難しく見えますが、改めて考えれば関数と性質としては当然のように思えるはずです。

理解を深めるために他の例を見てましょう。(*)
次の実装を考えてみます。
[画面上にボタンを10個JSで動的に配置する。各ボタンはクリックすると自身の番号をalert表示する。]

なんとなくで次のように書くと期待通りの動きをしてくれません。

(function(){
  for (var i = 0; i < 10; i++) {
    var button = document.createElement("button");
    button.innerHTML = i + '番';
    button.addEventListener("click", function(){
      alert(i);
    },false);
    document.body.appendChild(button);
  }
})();

一見するとうまく動きそうですが、どのボタンを押しても10と表示されてしまいます。
これはボタンのコールバック関数がクロージャだからです。
ループの中で各クロージャは同じ変数iを参照しており、ボタンがクリックされた段階ではループの処理は全て完了しています。
このため、クリックすると最終的なiの値である10が表示されることになります。

これを避けるには次のようにスコープを切ってあげればOKです。

(function(){
  function makeClickCallBack(count) {
    return function() {
      alert(count)
    }
  }

  for (var i = 0; i < 10; i++) {
    var button = document.createElement("button");
    button.innerHTML = i + '番';
    button.addEventListener("click", makeClickCallBack(i),false);
    document.body.appendChild(button);
  }
})();

 

クロージャは他の言語から来ると慣れるまでアレルギーが出そうなテーマですが、
「オブジェクトのように状態を保てる特別な関数がクロージャなんだな」というところまでわかってくるとあとは実践です。

クロージャはJavaScriptのコーディングをなんとなくで終わらせないためにもしっかりおさえたいところです。

 
 
*:ES2016からlet構文が導入されることでこの例も微妙になっちゃうのかなぁ…。

  • このエントリーをはてなブックマークに追加

PAGE TOP