【実務・中級編】プライベートクラスフィールド(#)のスコープ隔離:クロージャを使わずに実現する真のプライベート変数の仕組み – JavaScriptコア文法・モダン言語仕様と変数・関数解析バイブル

コードレビューをしていて、未だに「プライベート変数=クロージャ」という思考停止に陥っているコードを見かけると、私は少し悲しくなる。

「関数スコープの中に隠ぺいすれば安全だ」——確かにそれはES5時代、あるいはES2015前夜までのベストプラクティスだった。しかし、現代のJavaScriptランタイム、特にV8エンジンとそのメモリモデルを理解していれば、クロージャによるカプセル化がどれほどのコストを払い、どれほどの脆弱性をはらんでいるかは自明のはずだ。

今回は、ES2022で正式に標準化されたプライベートクラスフィールド(`#`構文)を取り上げる。これが単なる「シンタックスシュガー」ではなく、言語仕様の根幹から変数を隔離し、V8の最適化パイプラインにどう貢献するのか。クロージャとの比較を交えながら、プロダクションで即座に使える堅牢な設計論を授けよう。

—

1. なぜクロージャによるカプセル化は「悪手」になり得るのか

まずは、かつての王道であったクロージャによるプライベート変数実装を見てほしい。

// 【レガシーな設計】クロージャによるカプセル化
function createCounter() {
let count = 0; // プライベート変数にしたい

return {
increment() {
count++;
return count;
},
getCount() {
count;
}
};
}

const counter = createCounter();

このコードの何が問題か。フロントエンドのコンポーネント設計や、数千・数万個のインスタンスを生成するデータモデルの文脈において、これはメモリリークとパフォーマンス劣化の爆弾となる。

V8エンジンの視点:コンテキストの保持と隠れたコスト

1. スコープチェインの維持: クロージャは、外側関数のLexical Environment(字句環境)への参照をメモリ上に保持し続ける。GC(ガベージコレクション)は、内部関数が生存している限り、外側のローカル変数(`count`等)を解放できない。
2. Hidden Classes(隠れクラス)の崩壊: V8はオブジェクトのプロパティ構造を最適化するためにHidden Class(形状)を付与するが、ファクトリー関数やクロージャ経由で生成されるオブジェクトは、V8のインラインキャッシュ(Inline Caching)の効きを悪くし、メモリフットプリントを増大させる。
3. プロトタイプ共有の欠如: インスタンスごとにメソッドがクロージャのスコープをバインドして生成されるため、メモリ上でメソッドの重複が生じる(プロトタイプチェーンによるメソッド共有の恩恵を受けにくい)。

—

2. プライベートクラスフィールド(`#`)のメカニズム

では、ネイティブのプライベートクラスフィールド(`#count`)はどうだろうか。

class ModernCounter {
#count = 0; // 真のプライベートフィールド

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

getCount() {
return this.#count;
}
}

この記法がもたらす革新性は、単に「外部からアクセスできない」ことではない。言語仕様(ECMAScript)レベルでスコープとストレージが完全に隔離されている点にある。

V8ランタイムにおける内部構造

  • Slotベースのストレージ: `#`で宣言されたフィールドは、オブジェクトの通常のプロパティ辞書(Properties / Elements)には格納されない。オブジェクトのインスタンススロット(内部的な隠しスロット)に直接バインドされる。
  • 外側からのアクセス不能性: プロトタイプ汚染や、`Object.keys()`、`Reflect.ownKeys()`、果てはDevToolsのコンソールからすら、オブジェクトの構造をハックして外部から直接覗き見ることは不可能である(※WeakMapを用いたエミュレーションとは異なり、言語構文レベルで強固に守られている)。
  • ネイティブの速度: V8はプライベートフィールドへのアクセスを最適化されたオフセットアクセスとしてコンパイルするため、通常のプロパティアクセスやクロージャ経由のアクセスに比べてオーバーヘッドが極めて少ない。

—

3. 【プロダクションコード例】堅牢な非同期APIクライアントの設計

理論はここまでにしておこう。実務の現場で即座に応用できる、堅牢で美しいプロダクションコードを示す。

ここでは、リトライ機構、トークン管理、および内部状態の隠蔽を完璧にこなす「セキュアなAPIクライアント」をクラスベースで実装する。

/

  • 高度にカプセル化された堅牢なAPIクライアント
  • @class SecureApiClient

/
class SecureApiClient {
// — プライベートフィールド(言語レベルで完全に隔離) —
#baseUrl;
#authToken;
#maxRetries;

/

  • @param {string} baseUrl
  • @param {string} initialToken
  • @param {number} [maxRetries=3]

/
constructor(baseUrl, initialToken, maxRetries = 3) {
this.#baseUrl = this.#validateUrl(baseUrl);
this.#authToken = initialToken;
this.#maxRetries = maxRetries;
}

/

  • 内部バリデーション(外部から呼び出せないプライベートメソッド)
  • @param {string} url
  • @returns {string}

/
#validateUrl(url) {
const parsed = new URL(url);
if (parsed.protocol !== ‘https:’ && process.env.NODE_ENV === ‘production’) {
throw new Error(‘プロダクション環境では HTTPS が必須です。’);
}
// 末尾のスラッシュを除去して正規化
return url.replace(/\/+$/, ”);
}

/

  • 内部リトライロジック
  • @param {Function} fn
  • @returns {Promise}

/
async #executeWithRetry(fn) {
let attempts = 0;
while (attempts < this.#maxRetries) { try { return await fn(); } catch (error) { attempts++; if (attempts >= this.#maxRetries) {
throw new Error(`APIリクエストが最大試行回数 (${this.#maxRetries}回) を超過しました: ${error.message}`);
}
// 指数バックオフによる待機 (100ms, 200ms, 400ms…)
await new Promise(resolve => setTimeout(resolve, 100 Math.pow(2, attempts)));
}
}
}

/