りゅうじの学習blog

学習したことをアウトプットしていきます。

2022年2月25日JavaScript クロージャーについて

JS primerで毎日学習しておりますが、クロージャーという関数の概念が難しかったのでまとめておきます。

クロージャーとは

外側のスコープにある変数への参照を保持できるという関数の持つ性質のことです。

JS primerでの例となっているコードを引用します。

// `increment`関数を定義して返す関数
function createCounter() {
    let count = 0;
    // `increment`関数は`count`変数を参照
    function increment() {
        count = count + 1;
        return count;
    }
    return increment;
}
// `myCounter`は`createCounter`が返した関数を参照
const myCounter = createCounter();
myCounter(); // => 1
myCounter(); // => 2
// 新しく`newCounter`を定義する
const newCounter = createCounter();
newCounter(); // => 1
newCounter(); // => 2
// `myCounter`と`newCounter`は別々の状態持っている
myCounter(); // => 3
newCounter(); // => 3
  • createCounter関数が関数内で定義されたincrement関数の結果を返しています。
  • 関数の結果をmyCounter変数に代入しています。
  • myCounter変数を実行するたびにincrement関数内で1プラスされた値が返ってきています。
  • newCounter変数にcreateCounter関数を代入しています。
  • myCounter変数とは値の共有はしていません。

このようにまるで関数がcountという1ずつ増える値を持っているかのように振る舞えるの背景にクロージャーという仕組みがあります。

クロージャーを理解するために必要不可欠な2つの概念があります。

  • 静的スコープ
  • メモリ管理の仕組み

があります。

静的スコープ

const x = 10; // *1

function printX() {
    // この識別子`x`は常に *1 の変数`x`を参照する
    console.log(x); // => 10
}

function run() {
    const x = 20; // *2
    printX(); // 常に10が出力される
}

run();
  • run関数を実行しています。
  • run関数の中に定数xが定義されており、printX関数が実行されています。
  • printX関数内ではxを出力されていますが関数内にxの定義はありません。
  • スコープチェーンの仕組みで外側のスコープにxを探索しにいきxの結果は10が返されています。

printX関数をどこで実行したとしても結果のxが常に10を返すところが静的スコープを表しているところです。

(ちなみに動的スコープの言語ならばrun関数を実行しているのでprintXの結果のxはrun関数内で定義されている const x = 20 の結果である20を返します。)

このように、 どの識別子がどの変数を参照しているかを静的に決定する性質静的スコープ と呼びます。

メモリ管理の仕組み

  • 使わなくなった変数やデータを解放する仕組みです。
  • どこからも参照されなくなったデータを不要なデータと判断して自動的にメモリ上から解放する仕組みであるガベージコレクションJavaScriptでは採用されています。
function printX() {
    const x = "X";
    console.log(x); // => "X"
}

printX();
// この時点で`"X"`を参照するものはなくなる -> 解放される

とても当然の結果ですがこのようにデータが破棄されるのにはメモリ管理の仕組みがあるお陰なのです。

仕組みを理解した上で大切なのが、ここからのデータを破棄されないようにする方法です。

function createArray() {
    const tempArray = [1, 2, 3];
    return tempArray;
}
const array = createArray();
console.log(array); // => [1, 2, 3]
// 変数`array`が`[1, 2, 3]`という値を参照している -> 解放されない
  • createArray関数の結果を定数arrayに代入しています。
  • arrayの結果の [1, 2, 3] の結果をarrayに保存しています。

createArray();を実行した分だけ [1, 2, 3]の結果は出力できますが、全て別のものになってしまいます。

ここで大事なのは一度目に実行したcreateArrayの結果をarrayは保持していて、arrayを出力するたびに出力される[1, 2, 3]は同じものであるというところです。

つまり、関数の実行が終了したことと関数内で定義したデータの解放のタイミングは直接関係ないことがわかります。 そのデータがメモリ上から解放されるかどうかはあくまで、そのデータが参照されているかによって決定されます。

クロージャーがなぜ動くのか

クロージャーとは静的スコープとメモリ管理の仕組みを利用して、関数内から特定の変数を参照し続けることで関数が状態を持てる仕組みのことを言います。

const createCounter = () => {
    let count = 0;
    return function increment() {
        // `increment`関数は`createCounter`関数のスコープに定義された`変数`count`を参照している
        count = count + 1;
        return count;
    };
};
// createCounter()の実行結果は、内側で定義されていた`increment`関数
const myCounter = createCounter();
// myCounter関数の実行結果は`count`の評価結果
console.log(myCounter()); // => 1
console.log(myCounter()); // => 2
  • myCounter変数はcreateCounter関数の返り値であるincrement関数を参照している
  • myCounter変数はincrement関数を経由してcount変数を参照している
  • myCounter変数を実行した後もcount変数への参照は保たれている

myCounter → increment → count

  • count変数を参照するものがあるため、count変数は自動的に解放されません。
  • count変数の値は保持され続け、myCounter変数を実行するたびに1ずつ大きくなっていきます。
  • count変数が自動解放されずに保持できているのはincrement関数内から外側のcreateCounter関数スコープにあるcount変数を参照しているためです。

常に頭の中を整理していないとわからなくなってくるのではないでしょうか。

私はポケモンに例えて整理していました。

  • myCounterはモンスターボールである。
  • incrementはHPを回復させるキズグスリである。
  • countはHPである。
  • createCounterは野生のポケモンである。

野生のポケモンと出会ったらモンスターボールに入れないと同じポケモンにはもう会えませんよね。

モンスターボールに入れる事で保持できます。

increment関数で回復させたHPの値を保持しているのはモンスターボールに入れたポケモンに対してだから行えています。

クロージャーとはモンスターボールと同じ働きをしています。

以下、別の変数に代入した場合のコード例です。

const createCounter = () => {
    let count = 0;
    return function increment() {
        // 変数`count`を参照し続けている
        count = count + 1;
        return count;
    };
};
// countUpとnewCountUpはそれぞれ別のincrement関数(内側にあるのも別のcount変数)
const countUp = createCounter();
const newCountUp = createCounter();
// 参照してる関数(オブジェクト)は別であるため===は一致しない
console.log(countUp === newCountUp);// false
// それぞれの状態も別となる
console.log(countUp()); // => 1
console.log(newCountUp()); // => 1
  • createCounter関数を const countUpconst newCountUp の二つに代入しています。
  • この2つは別々のものです。

モンスターボールの例えを思い出すとすぐにわかります。

2匹のコラッタを別のモンスターボールに捕まえた状態です。

コラッタコラッタでも同じコラッタではないし、キズグスリを使った量で双方のHPは変わります。

最後に

今回はクロージャーがどういったものなのかをまとめました。用途に関しては触れていません。 細かくこの変数の値をどういった経緯で保持しているか?というところまで思考を深めてみましたが完全には理解できていない部分もあります。 しかし、私と同様に完全に理解できない方も現段階ではこういった関数の書き方をすれば値を保持できると覚えておくと良いと思います。

読んでくださった方、ありがとうございます。