HOWTO · JavaScript

JavaScript の static 変数

このチュートリアルでは、JavaScript の static 変数を紹介します。

このページの内容

JavaScriptでは、クラスのpublicなstaticフィールド、関数オブジェクトのプロパティ、クロージャによって保持されるローカル変数を使い、呼び出しの間も値を保持できます。どの方法でも状態は維持されますが、状態がどこに属するか、誰がアクセスできるかが異なります。クラス全体で共有する状態にはクラスフィールド、関数に紐づくシンプルな公開状態には関数プロパティ、値のバインディングを直接読んだり変更したりさせずに使わせる場合はクロージャを選びます。

JavaScriptにおけるstatic変数の意味

static変数とは、特定の呼び出しではなく、関数やクラスに紐づく状態を指します。JavaScriptには、あらゆる関数に使える共通のstatic変数宣言はありません。代わりに、いくつかのパターンがあります。staticキーワードはクラス自体にクラスフィールドやメソッドを宣言します。関数に代入したプロパティは、その関数オブジェクトに属します。クロージャでは、外側の関数が戻った後も、内側の関数からローカル変数を使い続けられます。

これらは関連する仕組みですが、すべての点で同じように使えるわけではありません。クラスフィールドと関数プロパティには、それぞれを所有するクラスや関数オブジェクトを通じてアクセスできます。クロージャは変数のバインディングを非公開にし、選んだ操作だけを公開できます。

クラス全体で共有する状態にはstaticクラスフィールドを使う

publicなstaticクラスフィールドはクラスに属するため、すべてのインスタンスがクラス名を通じて同じフィールドを参照します。各インスタンスにコピーされるわけではありません。この構文を使うには、publicなstaticクラスフィールドに対応した実行環境が必要です。対応していない場合は、以下の関数プロパティまたはクロージャの方法を使ってください。

次の例を実行してください。

このセクションのコードと表示されるラベルや出力は英語のままです。

class VisitCounter {
  static count = 0;

  static recordVisit() {
    return ++VisitCounter.count;
  }
}

console.log(VisitCounter.recordVisit());
console.log(VisitCounter.recordVisit());
console.log(VisitCounter.count);
1
2
2

どちらの呼び出しも、同じVisitCounter.countを更新します。このフィールドはpublicなので、VisitCounter.countとして読み取ったり代入したりすることもできます。アクセスをクラス自身で制御したい場合は、#で始まる名前のprivate staticフィールドを使います。staticメンバーにアクセスするときは、new VisitCounter()のようなインスタンスではなくクラスを使います。

関数にプロパティを追加する

関数はオブジェクトなので、名前付き関数に呼び出しをまたいで保持されるプロパティを持たせられます。特定の関数に紐づくカウンターやキャッシュを簡潔に作る方法です。このプロパティはpublicです。他のコードからnextId.counterを参照したり変更したりできるため、非公開の状態を保つ方法ではありません。

このセクションのコードと表示されるラベルや出力は英語のままです。

function nextId() {
  nextId.counter = (nextId.counter ?? 0) + 1;
  return nextId.counter;
}

console.log(nextId());
console.log(nextId());
console.log(nextId.counter);
1
2
2

同じ考え方はコンストラクター関数にも使えます。Widget.countのようなプロパティはコンストラクター関数に属し、widget.nameのようなプロパティは個々のインスタンスに属します。各インスタンスにWidget.countのコピーが作られるわけではありません。この共有プロパティにはWidgetを通じてアクセスします。状態の所有先という点ではクラス全体の状態に似ていますが、JavaScriptの関数プロパティには、クラスを使う一部の言語にあるようなアクセス制御や型保証がすべて備わっているわけではありません。

非公開の状態にはクロージャを使う

クロージャを使うと、外側の関数が終了した後も、内側の関数からローカル変数を使い続けられます。IIFE(Immediately Invoked Function Expression、即時実行関数式)は外側のスコープをその場で作り、保持した値を更新する操作を返します。変数のバインディング自体はpublicなオブジェクトに置かれないため、外部のコードから直接読み取ったり代入したりできません。

このセクションのコードと表示されるラベルや出力は英語のままです。

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

console.log(counter());
console.log(counter());
1
2

この例では、counterがcountをクロージャに保持します。counter()を呼び出すたびに同じ値が増えるため、呼び出しの結果は順に1、2になります。値は非公開で、counterが公開インターフェースです。独立した状態が必要なら、別のファクトリー関数を呼び出し、新しいクロージャを作って返します。返されたcounterを呼ぶだけでは、既存の状態が更新されます。呼び出し側で値をリセットしたり確認したりする必要がある場合は、バインディング自体を公開する代わりに、明示的なメソッドを持つ小さなオブジェクトを返してください。

arguments.calleeを避ける理由

arguments.calleeは、関数のargumentsオブジェクトを通じて現在実行中の関数を参照します。以前は、名前付き関数のプロパティと同じように、その関数へプロパティを追加するパターンで使われていました。これは古い手法であり、strict modeでは禁止されています。そのため、ECMAScriptモジュールを含むstrict modeが適用されるコードでは失敗することがあります。名前付き関数を使うと関係が明確になり、クロージャならこの制限のあるプロパティに頼らず非公開の状態を保てます。新しいコードではarguments.calleeを使わず、上記のサポートされている方法を選んでください。