uhyohyo.net

JavaScript初級者から中級者になろう

九章第五回 クロージャ

このページの最終更新日:

クロージャとは

今回はクロージャについて解説します。これはオブジェクト指向とはちょっと趣向が異なる方向の話ですが、とりあえず気にしないことにしましょう。

クロージャとは、ある関数が作られたとき、そのときの環境が関数の中で保持されるということです。

環境とは、そこの処理でどの変数が使えるかということです。JavaScriptではスコープという用語で表されることもあります。例えば、


var a=3;
console.log(a);

という何の変哲もないコードですが、console.log(a);が実行されたときの環境では、変数aが使用できます。また、変数a以外にも全てのグローバル変数が使用できます。例えば何気なく使っているconsoleはグローバル変数ですね。

また、


function aaa(){
  var b = 3;
  console.log(a+b);
}
var a = 5;
aaa();

というコードでは、関数aaa内で変数a,bが使われています。ここで変数aはグローバル変数で、変数bはローカル変数です。

つまり、関数内の環境は、グローバル変数とその関数のローカル変数を使うことができるというわけです。一方、関数の外ではローカル変数は参照できません。

このようにある変数が使える有効範囲をスコープといいます。例えばプログラムは全てグローバルスコープに入っており、これはグローバル変数が使える範囲です。また、関数aaaの中のプログラムは関数aaaのスコープに入っており、このスコープにはローカル変数bが属しています。結局、関数aaaの中のプログラムはグローバルスコープと関数aaaのスコープという2つのスコープに属していることになります。

なお、この例のように、グローバルスコープ以外のスコープは関数により作られます。(ES2015ではブロックスコープが導入されましたが、それはまた今度解説します。)

そして、環境というのは今どこのスコープに入っているのかということに対応しています。

さて、最初の説明で、「関数が作られたとき」とあります。なので関数を作ってみましょう。


function aaa(){
  var a = 3;
  var bbb = function(){
    console.log(a);
  }
  bbb();
}
aaa();

関数aaa内で、ローカル変数aと関数bbbが作られています。varを付けて代入されているので、bbb自体もたローカル変数であることに注意してください。

関数bbbの中の環境はどうなっているでしょうか。実は、関数bbbは関数aaaの中にあるので、関数aaaのスコープにある変数(この場合は変数a)が使えます。上の例では関数bbbの中で変数aが使われています。これはとても直感的で自然な動作ですね。

なお、関数bbbも自分のスコープを持っており、ここに属するのは関数bbbの中でvarで宣言された変数です。このスコープに属する変数が使えるのはスコープ内、つまり関数bbbの中だけであり、その外では使えません。

さて、これだけだと別に普通だし、何の意味があるのか分かりません。本題は、次のサンプルにあります。


function aaa(){
  var a = 5;

  var bbb = function(){
    console.log(a);
  };
  return bbb;
}
var func = aaa();
func();

関数aaaは、ローカル変数aとbbbを作ったあと、bbbを返しています。

返ってきたbbbはグローバル変数funcに代入されます。そして、このコードでは最後にfuncを呼び出しています。funcにはbbbが入っていたのでこの呼び出しではaが表示されます。

ここでのポイントは、funcは関数aaaの外で実行されたのに、aaa内のローカル変数である変数aの内容が正しく表示されるという点です。これは、funcは関数aaaの中で作られた関数であるため、その環境(ローカル変数aが使える)を保持しているからです。

このように、関数の中身が実行されるときの環境は常にその関数が作られたときの環境+その関数自身のスコープになります。

ここで注目すべき点は、funcは関数の処理が終了してしまってもう干渉することができなくなったローカル変数aに干渉する手段を与えているということです。

今はconsole.logで表示するだけですが、次のようにするとどうでしょう。


function aaa(){
  var a = 5;
  var bbb = function(){
    a++;
      console.log(a);
  }
  return bbb;
}
var func = aaa();
func();

a++;が追加され、funcを呼び出すことでaを増やすことができるようになりました。

ここで、関数aaaの外ではfuncはaに干渉する唯一の手段であり、aを同時に2以上増やす手段がなく、またaを1増やすにはログをそのつど表示しなければなりません。aは、決して5より小さくなりません。例えばaを100にするには、ログを95回出さなければいけないということになります。

このように、クロージャは、ローカル変数に対する限定的なアクセスを提供する手段になります。また、関数がどのような環境で実行されるかというのはJavaScriptの動作を理解する上で重要ですからぜひ身につけておきましょう。

クロージャの活用

クロージャの活用法として以前から利用されてきたものは、thisと関連した使い方があります。

DOMとの絡みが出てきますが、次のサンプルを見てみましょう。


function MyDiv(name){
  this.div = document.createElement("div");
  this.name=name;
  this.div.textContent="このdiv要素は"+name+"です";
  document.body.appendChild(this.div);
}

var div1 = new MyDiv("div1");

新しいMyDivのインスタンスを作って、変数div1に代入しています。

インスタンスを作った時点で、コンストラクタMyDivが実行されます。ここでやっていることは、新しいdiv要素を作ってプロパティdivに代入し、テキストノードのメッセージも加えてbody要素に追加されます。

また、名前を引数で渡し、プロパティnameに代入しています。

その結果、これを実行すると、body要素に「このdiv要素はdiv1です」という中身のdiv要素が出現します。これだけでは何の意味もないですが、きっとこの後このdivを使って凄いことをするのでしょう。

そういった時に欠かせないのがイベントです。そこで、「クリックされたら自身の名前(this.name)を表示するというのをやってみましょう。

そこで、単純に考えると次のようになるでしょう。


function MyDiv(name){
  this.div = document.createElement("div");
  this.name=name;
  this.div.textContent="このdiv要素は"+name+"です";
  document.body.appendChild(this.div);

  this.div.addEventListener('click',function(e){
    console.log(this.name);
  },false);
}

var div1 = new MyDiv("div1");
  

さっき書いたことをそのままコードで表現するとこうなりますね。クリックされるとthis.nameが表示されます。

しかし、これはうまくいきません。実際にクリックすると表示された値はundefinedです。これは、thisのプロパティとしてnameなどというものは無かった、つまりthisは、MyDivのインスタンスを表していないということです。

この理由は、thisの値は関数呼び出し時に決まるからです。今thisがMyDivのインスタンスなのは、MyDivがコンストラクタとして呼ばれたからでした。しかし、今回のイベントハンドラは


function(e){
  console.log(this.name);
}

という関数であり、この関数がイベント発生時に呼ばれた時点でthisには別の値が入りもはやMyDivのインスタンスではなくなってしまうのです。

ちなみに、この場合thisには多分div要素が入っています。

それではどうするかというと、ここでクロージャの登場です。具体的には次のようにします。


function MyDiv(name){
  this.div = document.createElement("div");
  this.name=name;
  this.div.textContent="このdiv要素は"+name+"です";
  document.body.appendChild(this.div);

  var t = this;

  this.div.addEventListener('click',function(){
    console.log(t.name);
  },false);
}

var div1 = new MyDiv("div1");

これがやっていることは簡単です。thisが勝手に変わってしまうのなら、別の変数に入れて取っておけばいいのです。しかしこの動作の裏にはクロージャ機構の活躍があります。イベントハンドラが実行されるのは関数が実行されるより後のことであるにも関わらずその中で変数tを利用することができているのは、先に説明したようにこのイベントハンドラが作られたときの環境を保持しているからです。

また、少しややこしいですが、こんな書き方をする人も昔はいました。


function MyDiv(name){
  this.div = document.createElement("div");
  this.name=name;
  this.div.textContent="このdiv要素は"+name+"です";
  document.body.appendChild(this.div);

  this.div.addEventListener('click',(function(t){
    return function(e){
      console.log(t.name);
    };
  })(this),false);
}

var div1 = new MyDiv("div1");

何か複雑になりましたね。しかし、試してみると確かにクリックすると"div1"が表示されます。

やっていることは分かりにくいですが、要はMyDivのローカル変数にtが入るのを嫌って、無名関数によるスコープを一段噛ませています。

そう、以前は使われていました。今はあまり使われていないのです。

では今はどうかというと、呼ばれる時のthis値を指定するメソッドbindがあります。これを紹介するのはまた今度ですが、使用例だけ紹介しておきます。


function MyDiv(name){
  this.div = document.createElement("div");
  this.name=name;
  this.div.appendChild(document.createTextNode("このdiv要素は"+name+"です"));
  document.body.appendChild(this.div);

  this.div.addEventListener('click',(function(e){
    console.log(this.name);
  }).bind(this),false);
}

var div1 = new MyDiv("div1");

bindを使うメリットは、thisがthisのままとなり余計な変数が必要ないという点です。