JavaScript 入門

JS クロージャの一般的な活用方法

クロージャ(Closures)は、JavaScript において強力であると同時に、初学者が混乱しやすい機能の一つです。基本概念(外部関数の実行終了後も周囲のスコープを「記憶」し、変数にアクセスできること)はすでに学びましたが、実際の開発でどのように活用されているかを知ることは非常に重要です。

本セクションでは、クロージャが真価を発揮する実用的なシナリオをいくつか紹介します。これらのパターンを習得することで、より高度な JavaScript 開発への道が開けるでしょう。

1. クロージャによるプライベート変数のシミュレーション

JavaScript には、Java や C++ のような「プライベート変数」をサポートするキーワードが長らく存在しませんでした(現代の JS では # によるプライベートフィールドが導入されましたが、クロージャによる実装は今でも標準的です)。クロージャを使えば、特定の関数スコープ内からのみアクセス可能な変数を作成し、事実上の「プライベート」な状態を作り出すことができます。

1.1 基本例:カウンター

シンプルなカウンターを考えてみましょう。呼び出すたびに値が増加する関数を作りたいのですが、その値は関数の外から直接書き換えられたくない、というケースです。

function createCounter() {
  let count = 0; // この変数は createCounter 関数に対してプライベートです

  return {
    increment: function() {
      count++;
      return count;
    },
    decrement: function() {
      count--;
      return count;
    },
    getValue: function() {
      return count;
    }
  };
}

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

// 'count' に直接アクセスしようとするとエラー(または undefined)になります
// console.log(counter.count); // undefined

解説:

  • createCounter は内部で count 変数を定義しています。
  • 返却されるオブジェクトには incrementdecrementgetValue の 3 つのメソッドが含まれています。
  • これらのメソッドはそれぞれ count 変数に対してクロージャを形成しています。つまり、createCounter の実行が終わった後も count を「記憶」しており、アクセスや修正が可能です。
  • 重要なのは、countcreateCounter の外からは決して触れないという点です。これがカプセル化の真髄です。

1.2 応用例:銀行口座

この概念をさらに進めて、銀行口座のシナリオに適用してみましょう。残高(balance)を隠蔽し、預金(deposit)や引き出し(withdraw)といった特定の操作のみを許可します。

function createBankAccount(initialBalance) {
  let balance = initialBalance;

  return {
    deposit: function(amount) {
      if (amount > 0) {
        balance += amount;
        return `預金: ${amount}。新しい残高: ${balance}`;
      } else {
        return "預金額は正の数である必要があります。";
      }
    },
    withdraw: function(amount) {
      if (amount > 0 && amount <= balance) {
        balance -= amount;
        return `引き出し: ${amount}。新しい残高: ${balance}`;
      } else if (amount <= 0) {
        return "引き出し額は正の数である必要があります。";
      } else {
        return "残高が不足しています。";
      }
    },
    getBalance: function() {
      return balance;
    }
  };
}

const account = createBankAccount(100);
console.log(account.deposit(50));   // 出力: 預金: 50。新しい残高: 150
console.log(account.withdraw(20));  // 出力: 引き出し: 20。新しい残高: 130
console.log(account.getBalance());  // 出力: 130
console.log(account.withdraw(200)); // 出力: 残高が不足しています。

// 'balance' への直接アクセスは不可能です
// console.log(account.balance); // undefined

解説:

  • balance 変数は createBankAccount 関数に対してプライベートであり、返却されたメソッドを介してのみ操作可能です。
  • これにより、不正な書き換えからデータを保護し、安全なインターフェースを提供できます。

2. 非同期 JavaScript における状態の保持

setTimeoutsetInterval、イベントリスナーなどの非同期操作を扱う際、クロージャは極めて重要です。これらの操作は即座には実行されず、後で実行されるようにスケジュールされます。クロージャは、最終的に関数が実行される時に、現在のスコープの値を「記憶」しておく役割を果たします。

2.1 基本例:setTimeout のループ内の罠

ループ内で setTimeout を使用する際に陥りやすい罠があります。イベントループの仕組み上、遅延実行される関数がループ変数の「最終的な値」を参照してしまう問題です。

function delayedLogs() {
  for (var i = 1; i <= 3; i++) {
    setTimeout(function() {
      console.log("値: " + i);
    }, i * 1000);
  }
}
delayedLogs(); 
// 出力: 
// 値: 4 (1秒後、2秒後、3秒後にそれぞれ 4 が出力される)

なぜ問題が起きるのか:setTimeout のコールバックが実行される頃には、ループはすでに終了しており、i の値は 4 になっています。コールバック関数は変数 i そのものに対してクロージャを形成しているため、すべてのコールバックが同じ「最終的な i」を参照してしまいます。

これを解決するために、クロージャを使って反復ごとの i をキャプチャします。

function fixedDelayedLogs() {
  for (var i = 1; i <= 3; i++) {
    (function(j) { // 即時実行関数式 (IIFE)
      setTimeout(function() {
        console.log("修正後の値: " + j);
      }, j * 1000);
    })(i); // 現在の 'i' を引数として渡す
  }
}
fixedDelayedLogs(); 
// 出力: 修正後の値: 1, 修正後の値: 2, 修正後の値: 3

解説(解決の原理):

  • setTimeout を 即時実行関数式 (IIFE) で包んでいます。
  • IIFE は i を引数として受け取り、それを j と名付けます。
  • 内部のコールバックは j に対してクロージャを形成します。j は IIFE のパラメータであるため、反復ごとに固有の値が保持されます。
  • これにより、各コールバックは自分専用の正しいコピー(1, 2, 3)を持つことができます。

3. 応用例:ループ内でのイベントハンドラ

DOM 要素にループでイベントリスナーを追加する場合も同様です。例えば、複数のボタンを作成し、クリック時にそのインデックスを表示したいケースです。

<!DOCTYPE html>
<html>
<body>
  <div id="container"></div>
  <script>
    const container = document.getElementById('container');
    for (let i = 0; i < 3; i++) {
      const button = document.createElement('button');
      button.textContent = `ボタン ${i + 1}`;
      container.appendChild(button);

      // IIFE でループ変数 i を固定
      (function(buttonIndex) {
        button.addEventListener('click', function() {
          alert(`これはボタン ${buttonIndex + 1} です`);
        });
      })(i);
    }
  </script>
</body>
</html>

解説:

  • ループごとに IIFE を実行し、その時点の ibuttonIndex として閉じ込めています。
  • ボタンがクリックされたとき、イベントハンドラはクロージャを通じて正しい buttonIndex にアクセスできます。

4. 部分適用 (Partial Application) と カリー化 (Currying)

クロージャは、関数型プログラミングにおける強力なテクニックである「部分適用」と「カリー化」の基盤です。

4.1 部分適用 (Partial Application)

既存の関数の一部の引数をあらかじめ固定した新しい関数を作成することです。残りの引数は、新しい関数を呼び出すときに渡します。

function multiply(x, y) {
  return x * y;
}

function createMultiplier(x) {
  return function(y) {
    return multiply(x, y);
  };
}

const double = createMultiplier(2); // x を 2 に固定
const triple = createMultiplier(3); // x を 3 に固定

console.log(double(5)); // 出力: 10
console.log(triple(5)); // 出力: 15

解説:createMultiplier は引数 x を受け取り、新しい関数を返します。返された関数はクロージャによって x を保持しており、後から渡される y と掛け合わせることができます。

4.2 カリー化 (Currying)

複数の引数を取る関数を、「引数を 1 つだけ取る一連の関数」に変換する技術です。

function curryMultiply(x) {
  return function(y) {
    return function(z) {
      return x * y * z;
    };
  };
}

const result = curryMultiply(2)(3)(4);
console.log(result); // 出力: 24

解説:
チェーン内の各関数は、前の関数から渡された引数をクロージャによって保持し続け、最後の引数が揃った時点で計算を実行します。

5. イテレータ (Iterators)

クロージャを使用して、一連の値へのアクセスを制御するカスタムイテレータを作成できます。

function createIterator(array) {
  let index = 0;
  return {
    next: function() {
      if (index < array.length) {
        return { value: array[index++], done: false };
      } else {
        return { value: undefined, done: true };
      }
    }
  };
}

const myArray = [10, 20, 30];
const myIterator = createIterator(myArray);

console.log(myIterator.next()); // { value: 10, done: false }
console.log(myIterator.next()); // { value: 20, done: false }
console.log(myIterator.next()); // { value: 30, done: false }
console.log(myIterator.next()); // { value: undefined, done: true }

解説:

createIterator 内の index 変数はクロージャによって保持されています。next メソッドを呼び出すたびに index が更新され、次の要素が返されます。これにより、外部から index を操作されることなく、安全に配列を走査できます。