【JavaScript】クロージャを知るときが来た

first-image closure javascript

クロージャ。ちょいちょい目にするワードではないでしょうか。私は『値が残るやつ』ぐらいの知識しかありませんでした。

そんな私が最近、クロージャっぽいな!というコードにぶち当たりまして。まあ……それは全然クロージャではなかった(笑)のですが、これを機にクロージャについて調べてみました。

基本のコード

function outside() {
  var num = 0;
  console.log(num);

  return function inside() {
    ++num;
    console.log(num);
  }
}

var func = outside(); // 0
func(); // 1
func(); // 2

クロージャの解説でよく使われているコードです。このコードを基本のコードと命名します。

基本のコードは、funcを実行する度に出力される値が1ずつ増えるという動作になります。結果は単純明快なのですが、その仕組みについて知ろうとすると深みにハマってしまいます。ですので、最初からひとつひとつ処理を追ってゆくことにします。論理的思考ってやつです。

コードを実際に動かしたいときは、下記のサイトがおすすめです。

関数outsideの実行

var func = outside(); // 0

変数funcに関数outsideが代入され、同時に実行されています。関数outsideが実行されているので、中のconsole.log(num);0を出力しています。ここで私は、なぜ関数inside内のconsole.log(num);は何も出力していないんだ?と疑問に思いました。よく見たら、関数insideは定義されているだけなので、中の処理には進みませんよね。定義された関数insideがreturnでそのまま返されているだけです。

さて、この文が実行されると、変数funcには関数outsideが返した関数insideが代入されます。つまり、変数funcを実行するということは、関数insideを実行することになります。

numはどこから来たのか

func(); // 1

数値が入っている変数はnumしかありませんから、numが出力されているとしか考えられないわけですが、そのnumはどこから来たのでしょうか。これを理解するために少しコードを変えてみます。

function outside() {
  var num = 0;
  console.log(num);

  (function() {
    ++num;
    console.log(num);
  })();
}

outside();
// 0
// 1

関数insideが即時関数に変わっています。

この例では、関数outsideが実行されると、まず関数outside内のconsole.log(num);0を出力します。その後、即時関数内でnumに1がプラスされ、即時関数内のconsole.log(num);1を出力します。

このとき、即時関数内のnumは、関数outside内で定義された変数numを参照しています。JavaScriptでは、外側で定義された変数を内側から参照することができます。ということは、基本のコードも内側から外側の変数を参照している、と考えれば良いわけです。

いやいや、ちょっとまってください。基本のコードでは、変数funcを実行すると関数insideそのものが実行されました。試しにconsole.log(func);とすると、やっぱり関数insideが出力されます。

関数outsideからは切り離されているのに、numは一体どこから来たのでしょうか。

スコープとスコープチェーン

ここで、購入してからざっと読んだだけの「開眼!JavaScript」という本を引っ張り出してきました。確かクロージャについて何か書いてあったはず。

スコープチェーンは、関数定義時の場所に基づいて決定されます。実行時ではありません。

開眼!JavaScript(100p)

定義済みの関数を別のスコープの変数に渡しても、スコープチェーンが変わることはありません。

開眼!JavaScript(101p)

スコープというのは定義された変数の有効範囲のこと(他にもevalスコープというものがあります)です。そして、外側で定義されている変数を内側から参照するとき、スコープを辿っていくことをスコープチェーンといいます。

関数の実行は関係ないんですね。関数の定義時に参照している変数は、いつでもどこでも使えますよと。内側から外側の変数を参照しているで当たってました。

ブロックスコープ

クロージャはES6よりも前の時代からあるテクニックなので、この記事ではあえて新しい記法でコードを書いていません。ですが、ES6からブロックスコープという、新しいスコープが実装されているので触れておきます。

{
  var v = 'var';
  let l = 'let';
  const c = 'const';
}

console.log(v); // var
console.log(l); // l is not defined
console.log(c); // c is not defined

ES6で登場したletconstは、ブロックの外側からは参照できません。

{
  var v = 'var';
  let l = 'let';
  const c = 'const';

  {
    console.log(v); // var
    console.log(l); // let
    console.log(c); // const
  }
}

letconstは、定義されたブロックと、その内側のブロックにスコープを持ちます。これがブロックスコープです。

値が保持されるのはなぜか

numを参照できる理由はわかりました。でも、定義時に参照が決まるのならnumは0だから、funcを実行したら0 + 1で常に1が出力されるはずでは。

この疑問を解決するには、JavaScriptという言語においてメモリがどのように管理されるのかを知る必要があります。

JavaScriptでは、変数を定義すると値がメモリに格納されます。格納された値は、変数が参照されている限りメモリに格納されたままとなります。変数が参照されなくなったら自動的にメモリから解放されます。プログラムにメモリを解放する処理を含める必要はありません。

サンプルコードを作ってみました。

var player = 'tom'; // tomがメモリに格納される
var winner = player; // winnerがplayerを参照
player = null; // tomとplayerの参照が切れるが、まだtomはメモリに格納されている(winnerから参照されているから)
winner = null; // playerへの参照がなくなったのでtomはメモリから解放される

では、このことを踏まえてクロージャの話に戻り、もう一度処理を追ってみます。

最初にfuncが実行されたとき、関数inside内でnumが定義されていないので、関数outsideのnumを参照します。このとき++numnum = 0 + 1です。つまり、numに1が代入されたことになります。このnumの値1は、関数insideが変数funcに参照されている限り、メモリに格納されたままです。ですので、次にfuncを実行したときはnum = 1 + 1となり、メモリに格納されている値が2に上書きされます。これこそ、値が増え続けるからくりです。

おわりに

良い勉強になりました。JavaScriptという言語そのものについて、理解が深まったという気がします。今回、入門書の方も読み返してみたんです。でも、クロージャについての記載はあるものの、スコープと参照までしか書いてないんですよね。どうりで、もやっとしか知らなかったわけだ。

お知らせ

現在お知らせはありません。

最近の投稿

シェア