JavaScript 入門

JS クロージャ応用:関数の状態を保持(Preserving State)する方法

クロージャ(Closures)は JavaScript における非常に強力な機能です。これにより、外部関数の実行が終了した後でも、その周囲のスコープ(Surrounding Scope)にある変数を関数が「記憶」することが可能になります。

この能力は、状態の保持(Preserving State)、データのカプセル化(Encapsulation)、および各種デザインパターンの実装において特に有用です。クロージャを使用して状態を保持する方法を理解することは、効率的でメンテナンス性の高い JavaScript コードを書くために不可欠です。

本章では、この仕組みを深く掘り下げ、実際の開発シナリオでこのコンセプトを活用するための様々な手法を紹介します。

1. クロージャによる状態保持の理解

本質的に、クロージャを利用した状態保持は、関数が自身のレキシカル環境(Lexical Environment)にアクセスできるという事実に依存しています。この環境には、関数が定義された時のスコープ内にあったすべての変数が含まれています。

次のコード例で、その動作を分解してみましょう。

function outerFunction() {
  let count = 0;
  function innerFunction() {
    count++;
    console.log(count);
  }
  return innerFunction;
}

const myCounter = outerFunction();
myCounter(); // 出力: 1
myCounter(); // 出力: 2
myCounter(); // 出力: 3

この例のポイントは以下の通りです:

  1. outerFunction が変数 count と内部関数 innerFunction を定義しています。
  2. innerFunctioncount の値をインクリメントし、コンソールに出力します。
  3. 重要なのは、outerFunctioninnerFunction をリターン(返却)している点です。

outerFunction() の実行結果を myCounter に代入すると、実際には innerFunction への参照(Reference)を代入したことになります。

innerFunction(現在は myCounter によって参照されている)が実行されるとき、outerFunction 本体の実行はすでに終了しているにもかかわらず、依然として outerFunction のスコープ内にある count 変数にアクセスできます。これがクロージャの本質です。つまり、innerFunctioncount 変数を「閉じ込めて(Closes over)」いるのです。

1.1 なぜ動作するのか:レキシカル環境

これを完全に理解するには、レキシカル作用域(Lexical Scope)を振り返る必要があります。

関数が定義されるとき、その周囲の環境へのリンクが作成されます。外部関数が実行を完了した後でも、このリンクは破棄されずに残ります。したがって、myCounter() を通じて innerFunction を呼び出す際、レキシカル環境内の count 変数にアクセスし、その値を修正することが可能なのです。

1.2 グローバルスコープとの比較

クロージャの重要性を強調するために、グローバル変数(Global Variables)を使用した場合にどうなるかを見てみましょう。

let globalCount = 0;

function incrementGlobalCount() {
  globalCount++;
  console.log(globalCount);
}

incrementGlobalCount(); // 出力: 1
incrementGlobalCount(); // 出力: 2

このコードでもカウント機能自体は実現できますが、globalCount 変数はコードのどこからでもアクセス・修正できてしまいます。そのため、予期せぬ値の書き換えが発生しやすく、ロジックの追跡が困難になります。

一方、クロージャを使用すれば count 変数を outerFunction の内部に封じ込め、プライベート(Private)な変数にすることができます。アクセスは innerFunction 経由に限定されます。このカプセル化こそが、クロージャで状態を保持する最大のメリットです。

2. クロージャによるプライベート変数の作成

JavaScript ではクロージャを利用して、他言語の private キーワードのようなプライベート変数をシミュレートすることが一般的です。JavaScript にはかつて真のプライベート変数を強制する仕組みがありませんでしたが(最近のクラス構文での # 記法を除く)、クロージャがその解決策を提供してきました。

function createCounter() {
  let count = 0; // この変数は createCounter 内部からしかアクセスできない
  return {
    increment: function() {
      count++;
    },
    decrement: function() {
      count--;
    },
    getValue: function() {
      return count;
    }
  };
}

const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getValue()); // 出力: 2

counter.count = 100; // count を直接修正しようとする試み
console.log(counter.getValue()); // 出力: 2 (count は変更されない)

この例では、createCounter は 3 つのメソッド(incrementdecrementgetValue)を含むオブジェクトを返します。

  • これらのメソッドはすべて、createCounter のスコープ内で定義された count 変数にアクセスできます。
  • しかし、count 変数自体を createCounter 関数の外部から直接参照することは不可能です。
  • counter.count = 100 としても、それは単にオブジェクトに新しいプロパティを追加しているだけで、クロージャの中に閉じ込められた本物の count 変数には何の影響も与えません。

これにより情報の隠蔽(Data Hiding)が実現され、状態がどのように変更・アクセスされるかを厳密に制御できるようになります。

2.1 プライベート変数のメリット

  • カプセル化 (Encapsulation): 内部データと実装の詳細を外部から隠します。これにより予期せぬ修正のリスクを減らし、コードを理解しやすくします。
  • モジュール化 (Modularity): 明確に定義されたインターフェースを持つ独立したコンポーネントを作成でき、再利用性が高まります。
  • 抽象化 (Abstraction): 低レイヤーの実装の複雑さを隠し、ユーザーが高い抽象レベルでオブジェクトと対話できるようにします。

3. 実戦的な例とデモンストレーション

様々なシナリオでクロージャがどのように状態を保持するか、具体的な実例を見ていきましょう。

3.1 示例 1:プライベートな状態を持つモジュールの作成

const myModule = (function() {
  let privateVariable = "こんにちは";
  
  function privateMethod() {
    console.log("プライベートメソッド内部: " + privateVariable);
  }
  
  return {
    publicMethod: function() {
      privateMethod();
    }
  };
})();

myModule.publicMethod(); // 出力: プライベートメソッド内部: こんにちは
// myModule.privateMethod(); // エラー: myModule.privateMethod is not a function
console.log(myModule.privateVariable); // 出力: undefined

この例では、即時実行関数式(IIFE)を使用してモジュールを作成しています。privateVariableprivateMethod は IIFE のスコープ内でのみアクセス可能です。publicMethod はモジュールの内部状態と対話するための唯一の手段を提供しており、カプセル化を実証しています。

3.2 示例 2:イベントハンドラとクロージャ

複数の要素にイベントリスナーを追加し、各リスナーがその要素に関連付けられた特定のデータにアクセスする必要があるシナリオを考えます。

function createButtonListeners() {
  for (let i = 0; i < 3; i++) {
    let button = document.createElement('button');
    button.innerText = 'ボタン ' + (i + 1);
    
    button.addEventListener('click', function() {
      console.log('ボタン ' + (i + 1) + ' がクリックされました!');
    });
    
    document.body.appendChild(button);
  }
}

createButtonListeners();

このコードにおいて、クロージャは各ボタンのクリックハンドラが自分に関連するインデックス i を正しく「記憶」することを保証します。
let キーワードを使用することで、ループの各反復(Iteration)ごとに新しい i のバインディングが作成され、そのバインディングがイベントリスナーのクロージャによってキャプチャされるため、各ボタンが正しい番号を出力できるのです。

3.3 示例 3:カスタム増分を持つカウンタ

function createCounterWithIncrement(incrementBy) {
  let count = 0;
  return {
    increment: function() {
      count += incrementBy;
      return count;
    },
    getValue: function() {
      return count;
    }
  };
}

const counterByTwo = createCounterWithIncrement(2);
console.log(counterByTwo.increment()); // 出力: 2
console.log(counterByTwo.increment()); // 出力: 4

const counterByFive = createCounterWithIncrement(5);
console.log(counterByFive.increment()); // 出力: 5
console.log(counterByFive.increment()); // 出力: 10

この例では、クロージャを使用して、それぞれが独自のカスタム増分値(incrementBy)を持つ複数のカウンタを作成する方法を示しています。

createCounterWithIncrement 関数は incrementBy パラメータを受け取ります。クロージャのおかげで、返された各オブジェクトは独自のプライベートな count 変数と、それに対応する固有の incrementBy 値を保持し続けることができます。