JavaScript / クロージャを少し噛み砕いてみる

2025/01/14に公開

クロージャとは

関数が宣言されたときのスコープを記憶し、そのスコープへのアクセスを保持する機能を指す。

通常、関数の実行後は内部で定義されている変数の値はメモリ領域から削除されてしまう。しかし以下のコードを見てみよう。

function outerFunction() {
    let outerVariable = "こんにちは!";

    function innerFunction() {
        console.log(outerVariable); // outerVariableにアクセス
    }

    return innerFunction;
}

const myClosure = outerFunction(); // > innerFunctionを受け取る
myClosure();// > こんにちは!
  1. outerFunctionを実行すると、innerFunction(関数)をreturnする。
  2. innerFunctionはouterFunctionの中で定義されており、outerVariableにアクセスしている。
  3. outerFunctionが実行を終えても、innerFunctionは外側のスコープ(outerVariable)を覚えているので、myClosureに代入された後でもその値にアクセスできる。

以上のコードからクロージャが再現できていることがわかる。
↓ まとめると以下となる。

クロージャの発生条件

  1. 関数が他の関数の内部で定義されている。
  2. 内部関数が、外部関数のスコープ内の変数の値を参照している。(内部関数からみたレキシカルスコープと言い換えることができる)

なぜクロージャが必要なのか?

  1. データの隠蔽(プライベート変数)
    外部からアクセスできない変数を作ることで、データを安全に保つことができる。
  2. 部分適用(カリー化)
    関数を部分適用することで、特定の変数を保持した状態で新しい関数を作成できます。

例えば以下のような、数字のカウントアップを実現する関数を見てみよう

function createCounter(startNum = 0) {
  let num = startNum;

  return () => {
    console.log(++num)
  };
}

const counter = createCounter(0);
counter(); // > 1
counter(); // > 2
counter(); // > 3

通常、関数の実行後は内部で定義されている変数の値は不要とみなされメモリ領域から削除されてしまうため、createCounterの実行後はnumの値もメモリ領域から削除される。
しかしクロージャにより、createCounterが実行された後も、numは無名関数の中からアクセス可能で、その値が保持され続ける。したがって、以下のように動作する

  1. counter()を呼び出すたびに、クロージャ内部でnumにアクセスして値をインクリメントする
  2. numの値は、呼び出しのたびに更新される

まとめ

関数の終了後、外部スコープが保持される仕組みが直感的ではないため、非常にわかりにくい機能だと思う。さらにスコープの概念などの必要前提知識も少なく無い。
しかしクロージャを理解できるようになれば、JavaScriptそのものへの理解もグッと深まるはずだ。

Discussion