JavaScript アドバンス

JavaScript クロージャ

1. JavaScript のスコープとバリアブル

JavaScript において、バリアブル(Variables) は以下のいずれかに属します:

  • ローカルスコープ (Local scope)
  • グローバルスコープ (Global scope)

クロージャ(Closures) を利用することで、グローバルなバリアブルをローカル(プライベート)に扱うことが可能になります。
クロージャは、ファンクション(関数)が「プライベート」なバリアブルを持つことを可能にします。

クロージャとは、外側のファンクションが実行を終了した後でも、そのファンクションが外側のスコープにあるバリアブルを保持(記憶)している状態を指します。

       注意: クロージャはアドバンスド(高度)なトピックです。先に進む前に、JavaScript のファンクションとスコープの基本を理解していることを確認してください。

2. ローカルバリアブル (Local Variables)

ローカルバリアブル とは、ファンクションの内部で定義された「プライベート」なバリアブルのことです。ファンクションは、自身のローカルスコープ内にあるすべてのバリアブルにアクセスできます。

2.1 ローカルバリアブルの例

function myFunction() {
  let a = 4; // a はローカルバリアブル
  return a * a;
}

3. グローバルバリアブル (Global Variables)

グローバルバリアブル とは、ファンクションの外側で定義された「パブリック」なバリアブルのことです。ファンクションは、グローバルスコープ内のすべてのバリアブルにアクセス可能です。

3.1 グローバルバリアブルの例

let a = 4; // a はグローバルバリアブル
function myFunction() {
  return a * a;
}

ウェブページにおいて、グローバルバリアブルはそのページ全体に属します。ページ内のすべてのスクリプトがグローバルバリアブルを使用(または変更)できます。

対照的に、ローカルバリアブルは定義されたファンクション内でのみ使用できます。他のファンクションやスクリプトからは隠蔽されており、プライベートな存在です。

ポイント:

  • 同じ名前のグローバルバリアブルとローカルバリアブルは、全く別のバリアブルとして扱われます。一方を変更しても、もう一方には影響しません。
  • var, let, const などのキーワードなしで作成されたバリアブルは、ファンクション内で作成されたとしても常にグローバルになります。

3.2 未宣言バリアブルの例

function myFunction() {
  a = 4; // キーワードがないため、グローバルバリアブルになる
}

4. バリアブルのライフタイム (Variable Lifetime)

バリアブルの「寿命(ライフタイム)」は以下の通りです:

  • グローバルバリアブル: ページを離れたり、ウィンドウを閉じたりするまで存続します。
  • ローカルバリアブル: 寿命は短いです。ファンクションが呼び出されたときに作成され、ファンクションの処理が完了したときに削除されます。

5. カウンター実装におけるジレンマ

何かをカウントするためのバリアブルが必要で、そのカウンターをすべてのファンクションから利用したいとします。

まずはグローバルバリアブルと、カウンターを増やすファンクションを使う方法が考えられます。

5.1 グローバルバリアブルによる実装例

// カウンターを初期化
let counter = 0;

// カウンターをインクリメントするファンクション
function add() {
  counter += 1;
}

// add() を3回呼び出す
add();
add();
add();

// カウンターの値は 3 になるはずです

警告: この方法には問題があります。ページ上のあらゆるコードが、add() を介さずに counter を直接変更できてしまいます。

そこで、他のコードから変更されないよう、counteradd() ファンクション内のローカルに置く必要があります。

5.2 ローカルバリアブルでの失敗例

let counter = 0;

function add() {
  let counter = 0; // ローカルで宣言
  counter += 1;
}

add();
add();
add();

// 期待値は 3 ですが、実際には 0 と表示されます(グローバルの方を見ているため)

これは、表示しているのがグローバルな counter であり、ローカルな counter ではないためです。また、ローカルで完結させようとしても別の問題が発生します。

5.3 戻り値を利用した失敗例

function add() {
  let counter = 0;
  counter += 1;
  return counter;
}

let x = 0;
x = add();
x = add();
x = add();

// カウンターの値は 1 になります。

ファンクションを呼び出すたびに let counter = 0 が実行され、カウンターがリセットされてしまうため、うまくいきません。

これを解決するのが、インナーファンクション(内側の関数) です。

6. JavaScript のネストされたファンクション

すべてのファンクションはグローバルスコープにアクセスできますが、JavaScript では自身の「一つ上」のスコープにもアクセス可能です。JavaScript はファンクションの入れ子(ネスト)をサポートしており、内側のファンクションは外側のスコープにアクセスできます。

6.1 ネストされたファンクションの例

function add() {
  let counter = 0;
  function plus() { counter += 1; } // 内側のファンクションが外側の counter にアクセス
  plus();   
  return counter;
}

もし外部から plus() ファンクションにアクセスでき、かつ counter = 0 を一度だけ実行する方法があれば、このカウンターのジレンマは解決します。

その解決策こそが クロージャ です。

7. JavaScript クロージャ (Closures)

7.1 クロージャの実装例

function myCounter() {
  let counter = 0;
  return function() {
    counter++;
    return counter;
  };
}

const add = myCounter();
add();
add();
add();

// カウンターの値は 3 になります

7.2 例の解説

  1. add というバリアブルに、myCounter ファンクションの戻り値をアサイン(代入)しています。
  2. myCounter は一度だけ実行されます。それは counter を 0 にセットし、ファンクション式(Function Expression) を返します。
  3. これにより add 自体がファンクションになります。
  4. ここで重要なのは、add が親スコープの counter に引き続きアクセスできるという点です。

これが クロージャ です。ファンクションが「プライベート」なバリアブルを持つことを可能にします。
countermyCounter のスコープによって保護されており、add ファンクションを通じてのみ変更可能です。

8. まとめ

クロージャとは、親ファンクションの処理が完了し「閉じられた」後でも、その親スコープにアクセスできるファンクションのことです。

歴史的にクロージャは以下の目的で使用されてきました:

  • プライベートバリアブルの作成
  • ファンクション呼び出し間でのステート(状態)の保持
  • letconst が存在しなかった時代におけるブロックスコープのシミュレーション
  • カリー化(Currying)やメモ化(Memoization)などのデザインパターンの実装

モダンな JavaScript では、これらの役割の一部を代替する新しい機能が登場しています。

9. モダンな代替案:プライベートクラスフィールド

ECMAScript 2022 以降、JavaScript は # 構文を用いた真の プライベートクラスフィールド をサポートしています。これらは言語仕様として強制されており、クラスの外側からは一切アクセスできません。

9.1 プライベートフィールドの例

class Counter {
  #count = 0; // # をつけることでプライベートになる

  increment() {
    this.#count++;
    return this.#count;
  }
}

const myCounter = new Counter();
myCounter.increment();

なぜプライベートクラスフィールドが推奨されるのか:

  • 真のプライバシーが保証されるため。
  • クラス内でクロージャベースのパターンを使うよりも記述がシンプルになるため。
  • ECMAScript の標準規格であるため。

クロージャは依然として、クラス外での処理や関数型プログラミングのパターンにおいて極めて重要で有用な技術です。特にレガシーなコードベースのメンテナンスにおいては必須の知識と言えるでしょう。