JavaScript 入門

JS クロージャの基礎

クロージャ (Closures) は、外部関数が実行を終了した後でも、その周囲のスコープ (Surrounding Scope) にある変数に関数がアクセスすることを可能にします。クロージャを理解することで、強力なプログラミングパターンを解き放ち、一般的な落とし穴を回避できるようになります。

本章では、クロージャとは何か、どのように機能するのか、そしてなぜそれほどまでに有用なのかを詳しく探っていきます。

1. クロージャを理解する:基礎知識

本質的に、クロージャは「関数」とその関数が宣言された「レキシカル環境 (Lexical Environment)」の組み合わせです。

この環境には、関数が作成された時点のスコープ内にあったすべての変数が含まれます。関数はこれらの変数を「閉じ込める (Closes over)」ため、元のスコープの外で実行されたとしても、それらの変数へのアクセス権を保持し続けます。

これを説明するために、シンプルな例を見てみましょう:

function outerFunction(outerVar) {
  function innerFunction(innerVar) {
    console.log(outerVar, innerVar);
  }
  return innerFunction;
}

const myInnerFunction = outerFunction("こんにちは");
myInnerFunction("世界"); // 出力: こんにちは 世界

この例では、innerFunction がクロージャです。outerFunction が値を返して終了した後でも、innerFunctionouterFunction のスコープから outerVar にアクセスできます。myInnerFunction を呼び出したとき、それは依然として outerVar の値を保持し、使用することができるのです。

2. レキシカル環境 とスコープ

クロージャをマスターするには、レキシカルスコープ (Lexical Scope) の理解が不可欠です。

レキシカルスコープとは、関数がソースコード内の「どこに配置されているか」によって、その関数のスコープが決定されることを意味します。上の例では、innerFunction はレキシカル(静的)に outerFunction の内部にネストされているため、outerFunction の変数にアクセスできます。

これは実行時のコールスタックによって決定される動的スコープ(JavaScript は動的スコープを採用していません)とは異なります。

3. クロージャが状態 (State) を保持する仕組み

クロージャは、関数がその閉じ込められたスコープ内の値を「記憶」することを可能にします。この状態を保持する (Preserve state) 能力は、クロージャの最も強力な機能の一つです。

別の例を見てみましょう:

function createCounter() {
  let count = 0;
  function increment() {
    count++;
    console.log(count);
  }
  return increment;
}

const counter1 = createCounter();
counter1(); // 出力: 1
counter1(); // 出力: 2

const counter2 = createCounter();
counter2(); // 出力: 1
counter1(); // 出力: 3

このケースでは、increment がクロージャであり、count 変数にアクセスしています。

createCounter を呼び出すたびに、独自のプライベートな count 変数を持つ新しいクロージャが作成されます。これにより、複数の独立したカウンタを作成することが可能になります。この count 変数は increment 関数を通してのみアクセスできるため、実質的にプライベートな変数となります。

4. 多重にネストされたクロージャ

クロージャはネストさせることができます。関数は、自分を囲んでいるすべての周囲のスコープから変数にアクセス可能です。

function grandParentFunction(grandParentVar) {
  function parentFunction(parentVar) {
    function childFunction(childVar) {
      console.log(grandParentVar, parentVar, childVar);
    }
    return childFunction;
  }
  return parentFunction;
}

const parent = grandParentFunction("祖父 (GrandParent)");
const child = parent("父親 (Parent)");
child("子供 (Child)"); // 出力: 祖父 (GrandParent) 父親 (Parent) 子供 (Child)

ここでは、childFunctiongrandParentFunctionparentFunction の両方から変数にアクセスできています。

5. クロージャの実践的な例

クロージャの理解を深めるために、より実戦に近い例を見ていきましょう。

5.1 カプセル化 (Encapsulation)

function createBankAccount(initialBalance) {
  let balance = initialBalance;
  return {
    deposit: function(amount) {
      balance += amount;
      return balance;
    },
    withdraw: function(amount) {
      if (balance >= amount) {
        balance -= amount;
        return balance;
      } else {
        return "残高不足";
      }
    },
    getBalance: function() {
      return balance;
    }
  };
}

const myAccount = createBankAccount(100);
console.log(myAccount.deposit(50));   // 出力: 150
console.log(myAccount.withdraw(20));  // 出力: 130
console.log(myAccount.getBalance());  // 出力: 130

// console.log(myAccount.balance); // undefined. balanceはクロージャによってプライベート化されています

この例では、balance 変数は createBankAccount によって作成されたクロージャに対してプライベートです。外部のコードは balance に直接アクセスしたり修正したりすることはできず、deposit(預金)、withdraw(引き出し)、getBalance(残高確認)といったメソッドを介してのみ対話できます。これはカプセル化の一形態であり、オブジェクトの内部状態を保護するのに役立ちます。

5.2 モジュール (Modules) の作成

クロージャは、JavaScript においてモジュールを作成するためにも使用されます。モジュールは、データと振る舞いを含む独立したコード単位です。

const myModule = (function() {
  let privateVariable = "秘密 (Secret)";
  function privateMethod() {
    console.log("privateMethod 内部:", privateVariable);
  }
  return {
    publicMethod: function() {
      console.log("publicMethod 内部");
      privateMethod();
    }
  };
})();

myModule.publicMethod(); 
// 出力: publicMethod 内部
// privateMethod 内部: 秘密 (Secret)

// myModule.privateMethod(); // エラー - 関数ではありません
// console.log(myModule.privateVariable); // undefined

この例では、IIFE(即時実行関数式)がクロージャを作成し、privateVariableprivateMethod をカプセル化しています。これらはモジュール外部からはアクセスできません。モジュールは、プライベートメンバにアクセスできる publicMethod のみを公開しています。

5.3 イベントハンドラ (Event Handlers)

クロージャは、イベントハンドラが作成された時点で使用可能だったデータにアクセスするために、イベントハンドラでよく使用されます。

<!DOCTYPE html>
<html>
<head>
<title>クロージャの例</title>
</head>
<body>
  <button id="myButton">ここをクリック</button>
  <script>
    function setupButton(buttonId, message) {
      const button = document.getElementById(buttonId);
      button.addEventListener("click", function() {
        alert(message);
      });
    }
    setupButton("myButton", "ボタンがクリックされました!");
  </script>
</body>
</html>

この例で addEventListener に渡されている匿名関数はクロージャです。この関数は setupButton 関数の message 変数にアクセスできます。setupButton の実行が完了した後でも、イベントハンドラは依然として message の値を保持し、使用することができます。