8
Help us understand the problem. What are the problem?

posted at

updated at

【JavaScript】JavaScriptにおけるクロージャーとは

※当方駆け出しエンジニアのため、間違っていることも多々あると思いますので、ご了承ください。また、間違いに気付いた方はご一報いただけると幸いです。

#クロージャーとは
####「外側のスコープにある変数への参照を保持できる」という関数が持つ性質のこと。
...なんのこっちゃ。

実際にクロージャーを用いたコード

function outside() {

    let count = 0;

    function inside() {
        count = count + 1;
        return count;
    }
    return inside;
}

このような関数があるとします。
この時、

const myCounter = outside();
console.log(myCounter);

このように、outsideを実行して、変数myCounterに格納し、中身をみてみると。

>>  function inside() {
        count = count + 1;
        return count;
    }

当然、関数が返ってきます。

では、下のようにmyCounterを呼び出した結果をみてみると

console.log(myCounter());

>> 1

この時、countは、 「スコープチェーン」 によりoutside配下のcountの参照します。
スコープチェーンとは、
変数を参照する際には、現在のスコープから外側のスコープへと順番に変数が定義されているかを確認することです。
つまり、現在のスコープ

    function inside() {  //ここ  }

に、countなんて定義されてないぞ!!しゃーない、外に見に行くか。はー、めんどくさ。

function outside() {
    let count = 0;
}

おお、すぐいた!!
ということで、一つ外側のスコープoutsideにcountが定義されてたので
めでたく count が 0であることが分かりました。
そして、insideで定義された処理を実行した結果、当然返り値は

>>1

となるのです。
では、二回実行するとどうなるか。

console.log(myCounter());
console.log(myCounter());

>> 1
>> 2

となります。。。。なんでや。

これこそが 「クロージャー」 の動きとなります。

なぜ、このような挙動をとるのか。

JavaScriptの「静的スコープ」と「メモリ管理の仕組み」によります。

##「静的スコープ」について
JavaScriptのスコープには、どの識別子がどの変数を参照するかが静的に決定されるという性質があります。

つまり、どこでinside関数を呼び出そうが、inside内のcountは、outside直下のcountを参照するということですね。

**「this」**という特別なキーワードだけは、呼び出し元によって動的に参照先が変わります。

##「メモリ管理の仕組み」について
変数のデータはパソコンの上のメモリに保存されます。これは、決まったタイミングで解放しないといけません。メモリは有限だからです。
JavaScriptでは 「ガベージコレクション」 という「どこからも参照されなくなったデータを不要なデータと判断して自動的にメモリ上から解放する仕組み」をとっています。

逆にいうと、参照され続けている限りメモリには保存され続けます。
「関数の中で作成したデータは、その関数の実行が終了したら解放される」訳ではありません。

例えば


function outX () {
  let x = 1;
  console.log(x);
}
outx();

この関数が実行された時、xが参照する1が保管されたメモリが確保されますが、関数終了後に、このメモリは解放されます。これは**「関数が終了したから」ではなく「xが参照されなくなったから」**です。

よって表題例の場合
内側の関数内から、外側の関数内の変数countを参照しているため、メモリが解放されず値を保持し続けるということですね。

よって一回目の

myCounter();

が実行された段階で、countには 1 が格納され、メモリ上に保持されるので

二回目に

myCounter();

が呼び出された時、返り値は 2 となります。

また、このように再度outside()を実行しなおしすと、

const myCounter = outside();
const myCounter2 = outside();

console.log(myCounter()); // => 1
console.log(myCounter2()); // => 1

と、別々の関数が作り直されて let count = 0が 各々で実行されるので、当然結果は上記のようになります。

##こんなことしなくても、関数内でプロパティーを定義したらいいやん。

と思われるかもしれません。

下記のcountUp関数は、自身のプロパティcountの値に +1 をして、呼び出し元に返却する関数です。

function countUp() {
    // countプロパティを参照して変更する
    countUp.count = countUp.count + 1;
    return countUp.count;
}
// 関数オブジェクトにプロパティとして値を代入する
countUp.count = 0;
// 呼び出すごとにcountが更新される
console.log(countUp()); // => 1
console.log(countUp()); // => 2

これでも、プロパティとして値を保持し続けます。しかしクロージャーとの違いは、

**関数の外からでも値を変更できてしまう。**ということです。

countUp.count = 0;

そもそもこの式が、外から関数のプロパティーの値を操作してますよね。
関数をconstで 変数に格納して定義したとしても、関数もオブジェクトでありミュータブルであるため、プロパティの値の変更は可能です。

詳しくはこっちの記事をごらんください。

上記のクロージャーだと、関数の外から直接countの値を変更することはできません。

関数の外から count = 100; みたいなことをしても 関数スコープに阻まれるので操作できないのですね。

これこそがクロージャー(「閉ざされた。」)の由来であり、一番のメリットであります。この辺は勝手な想像ですが。

ということで 自分なりに学習したクロージャーについてでした。

。。。合ってますか?

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
8
Help us understand the problem. What are the problem?