uhyohyo.net

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

九章第五回 クロージャ

クロージャとは

今回はクロージャについて解説します。

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

正式名称ではないとからしいですが、クロージャということでいきます。

さて、環境とは、そこの処理でどの変数が使えるかということです。例えば、

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

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

また、

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

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

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

今回、このローカル変数がポイントです。

最初の説明で、「関数が作られたとき」とあります。関数を作ってみましょう。ローカル変数がポイントなので、ローカル変数が使える環境にある関数内で関数を作ってみます。

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

関数aaa内で、ローカル変数aと関数bbbが作られています。varを付けて代入されているので、bbbもまたローカル変数であり、関数内でしか利用できません。

関数bbbの中では、関数が作られた環境(aaa内の環境)を保持しているため、グローバル変数・aaa内のローカル変数が利用できます。また、さらに、bbb内でローカル変数を作ればそれも利用できます。

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

function aaa(){
  var a = 5;
  	return bbb;

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

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

function 関数名(){
}

の形を使用していますが、実はこういうこともできます。こうすると、先の例と同じようにローカル変数のようになります。

さて、返ってきたbbbは、今度はcccに代入されます。そしてそれを呼び出しています。このbbbは、aをログするだけです。

ここで、cccは関数aaaの外で実行されたのに、aの内容が正しく表示されます。これは、ccc、つまりbbbが、関数aaaの中で作られたため、その環境(ローカル変数aが使える)を保持しているからです。

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

今はconsole.logで表示するだけですが、これはどうでしょう。

function aaa(){
  var a = 5;
  return bbb;

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

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

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

aを100にするには、ログを95回出さなければいけないということです。

このように、クロージャは、想定外の動作を絶対にされたくない変数を保護したり、ローカル変数の内容を関数の外に伝えたりするのに使うことができます。

クロージャの活用

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

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

<script type="text/javascript">
  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");
</script>
        

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

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

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

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

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

イベントについては、第三章を参照して下さい。

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

<script type="text/javascript">
  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");
</script>
        

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

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

これはひょっとすると、イベントとして登録された関数(イベントハンドラ)がインスタンスのメソッドでは無いからかもしれません。ということでメソッドにしてみましょう。

<script type="text/javascript">
  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',this.clicked,false);
  }
  //クリックされたときに呼ばれるメソッド
  MyDiv.prototype.clicked=function(e){
  console.log(this.name);
  };

  var div1 = new MyDiv("div1");
</script>
      

しかし、結果はまだundefinedです。

何が起こっているかというと、このthisというのは、基本的に

インスタンス.メソッド名();

というような形で呼ばれないとthisがインスタンスとしてセットされないのです。

しかし、イベントハンドラとして関数が呼ばれる場合は、この形で呼ばれることはありません。だから、最初にやった方法ではthisがインスタンスではなかったのです。

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

<script type="text/javascript">
  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(i){
      return function(e){
        console.log(i.name);
      };
    })(this),false);
  }

var div1 = new MyDiv("div1");
</script>
      

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

addEventListenerの第二引数で何が起こっているかというと、最後の(this)関数呼び出しです。呼び出されている関数は、その前の

(function(i){
  return function(e){
    console.log(i.name);
  };
})

です。括弧で囲まれていますが、function (){ }という無名関数の形ですね。括弧で囲んでいるのは、分かりやすくするためです。今回は括弧が無くても大丈夫ですが、場合によっては括弧がないと動作が変になります(関数式がFunctionDeclarationと間違われる場合)

それで、この無名関数は引数iを持ちます。この引数iにthisを渡していますね。この無名関数を仮に外側の無名関数と呼ぶことにします。

さて、この外側の無名関数が返すのは、またも無名関数です。ややこしいですが、これを内側の無名関数と呼びましょう。

この内側の無名関数は、外側の無名関数内で作られたのです。ここがポイントで、ここにクロージャが関わってきます。クロージャとは、ある関数が作られたとき、その関数が作られた環境を保持していて、その環境の変数を参照できるということでした。

ここで、作られた関数は内側の無名関数で、環境は外側の無名関数の環境です。

外側の無名関数で利用できる変数とは、そう、変数iです。外側の無名関数が呼ばれた時点で、その引数はthisです。このthisはコンストラクタ内で使用されたものであるから、このthisは確かにインスタンスを表します。つまり、ここでは変数iはMyDivのインスタンスであるのです。

ここで、クロージャにより、内側の無名関数から変数iを参照できるのです。内側の無名関数が作られた時点、つまり外側の無名関数が呼ばれた時点では変数iはインスタンスなわけですから、内側の無名関数はその環境を保持し、関数呼び出しが終了した後でも変数iはインスタンスとして残り続けるのです。

さて、内側の無名関数は、実は外側の無名関数の戻り値となっています。戻り値とは即ち関数呼び出しの結果ですから、それがaddEventListenerの第二引数となります。

内側の無名関数の働きは、iのプロパティnameをログすることでした。iはインスタンスですから、クリックするとインスタンスのプロパティnameが正しく表示されたのです。

ここで、結局何をしたかというと、クロージャを利用して、実際にイベントハンドラとしてクリック時に呼ばれる関数(内側の無名関数)内で、インスタンスへの参照(この場合は変数i)が保持されるようにしたのです。

このような手法は、良く利用されていました。少し前までは

ちなみに、実はこのような方法もあります。

<script type="text/javascript">
  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(e){
      console.log(t.name);
    },false);
  }

  var div1 = new MyDiv("div1");
</script>
    

これは、二重の無名関数が一重になりました。

ここでポイントは、変数tにthisを代入している点です。addEventListener第二引数の無名関数は、コンストラクタである関数MyDivの内部で作られたものですから、またMyDiv内の環境を保持しているわけです。

すなわち、addEventListenerの第二引数となった無名関数からは、いつでも変数tが参照できるのです。変数tとはインスタンスですから、そのプロパティを参照してやればいいということです。

なぜtなら良くてthisならだめなのかというと、thisは特別な存在で、関数呼び出し時にはその内側の環境で新しいthisが発生するからです。普通の変数ならばそのようなことは起こりません。

始めに紹介した複雑な方法には、余計なローカル変数(この場合t)をMyDivの環境に増やさないということがあります。このような方法が以前は使用されていました。

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

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

<script type="text/javascript">
  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");
</script>
    

bindを用いると、呼ばれた時のthis値を固定することができるのです。つまりこの場合、第二引数に渡された関数(にbindを適用した返り値)は、this値が本来とは異なりbindの第一引数の渡したものになるのです。これは比較的新しいもので、今の時代これが使われます。

しかし、クロージャのサンプルとしてはさっきの複雑な方法はいい例です。理解して使えるようになりましょう。