こんにちは!フロントエンドからNode.jsの深層まで、日々のコードがV8エンジン上でどう息づいているかを見つめ続けているシニアアーキテクトです。
今回は、モダンJavaScriptにおける最大のパラダイムシフトの一つである「プライベートクラスフィールド(#)」について、その本質を徹底的に解剖していきましょう。
他の言語(JavaやC#など)からJavaScriptに入ってきた開発者の方なら、「やっと真のカプセル化が来た!」と歓声を上げたかもしれません。しかし、この `#` 記号の裏側で、JavaScriptのランタイムやスコープの仕組みがどう動いているか、気になったことはありませんか?
「クロージャを使わなくても隠蔽ができる」と言われていますが、メモリ上では何が起きているのでしょうか?
今回は、初学者の方や他の言語からの移行組の方にもスッと腹落ちするように、優しく、かつエンジニアの魂を揺さぶる深さまで解説していきますね。ここをクリアすれば、あなたのJavaScriptのオブジェクト指向への理解は一段と深まりますよ!
—
1. そもそも、なぜ「プライベート」が必要だったのか?
JavaScriptには、長らく「本当の意味でのプライベートプロパティ」が存在しませんでした。
これまでは、開発者たちの間で様々な工夫(ハック)が使われてきました。
例えば、プロパティ名の先頭にアンダースコア(`_`)をつけて「プライベートだから外から触らないでね」とチーム全体で約束したり、クロージャを使って外から見えない変数をスコープ内に閉じ込めたりしていましたよね。
ちょっと昔のクロージャを使ったカプセル化のコードを思い出してみましょう。
function createCounter() {
// この count 変数は、外からは絶対にアクセスできない(クロージャによる隠蔽)
let count = 0;
return {
increment() {
count++;
return count;
},
getCount() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.count); // undefined (守られている!)
この方法は確かに強力ですが、「インスタンスを生成するたびにメソッドの関数オブジェクトがメモリ上に複製される」という、メモリ効率上の大きな弱点がありました。プロトタイプチェーンの恩恵を受けにくかったのです。
そこで登場したのが、ECMAScript 2022で正式に標準化されたプライベートクラスフィールド(#シンボル)です。
—
2. プライベートクラスフィールド(#)の基本的な使い方
まずは、モダンなクラス構文の中で、どのように `#` を使うのかを見てみましょう。使い方はとてもシンプルです。
class BankAccount {
// プライベートフィールドの宣言(#を頭につけるだけ!)
#balance = 0;
constructor(owner, initialBalance) {
this.owner = owner;
this.#balance = initialBalance;
}
// 入金メソッド
deposit(amount) {
if (amount <= 0) {
throw new Error("入金金額は0より大きくなерでください。");
}
this.#balance += amount;
return this.#balance;
}
// 残高確認メソッド
getBalance() {
return this.#balance;
}
}
const myAccount = new BankAccount("Alice", 1000);
console.log(myAccount.deposit(500)); // 1500
console.log(myAccount.getBalance()); // 1500
// 外から直接アクセスしようとすると…?
// console.log(myAccount.#balance);
// 🚨 SyntaxError: Private field '#balance' must be declared in an enclosing class
外から `#balance` に直接アクセスしようとすると、実行時ではなくパース(構文解析)の段階でエラー(SyntaxError)になります。これが、JavaScriptがコンパイル時にしっかりとコードの安全性を担保してくれている証拠です。
—
3. 陥りやすい文法エラーと「お作法」
ここで、初心者の人が非常によくハマるポイントをいくつかご紹介しておきますね。これを覚えておくだけで、無駄なデバッグ時間を大幅に削減できます。
エラー1: 事前宣言をしていない
従来のJavaScriptのクラスプロパティと違い、プライベートフィールドはクラスの本体のスコープ内で事前に `#` 付きで宣言しておかなければなりません。コンストラクタの中でいきなり `this.#foo = 10` のように書き始めることはできません(※最新の一部の環境を除き、基本は事前宣言が必須です)。
class User {
// ❌ 宣言せずにコンストラクタで使おうとするとエラーになる
// constructor(name) {
// this.#name = name;
// }
// ⭕️ 正しい書き方
#name;
constructor(name) {
this.#name = name;
}
}
エラー2: クラスの外から動的に追加できない
通常のJavaScriptのオブジェクトは、後から `obj.newProperty = “value”` のように動的にプロパティを生やすことができますよね。しかし、プライベートフィールドはクラスの外からは一切追加もアクセスもできません。
—
4. 【深掘り】# のスコープ隔離とメモリ管理の真実
ここからが本題、少し踏み込んだ「上級者向け」の解説です。
「一体、この `#` は内部でどうやって動いているのでしょうか?」
他の言語の多くは、クラスのインスタンスごとにメモリの領域があり、そこにアクセス修飾子(privateなど)の情報を紐付けています。しかし、JavaScript(V8エンジンなど)におけるプライベートフィールドは、「シンボル(Symbol)ベースの隠しプロパティ」ではありません。
もし単なる隠しプロパティ(列挙されないプロパティ)であれば、開発者ツールのコンソールや `Object.getOwnPropertySymbols()` で強引に中身を覗き見ることができてしまいます。しかし、プライベートフィールドは完全に隔離されたスコープ(WeakMap的な概念に近いアプローチ)で管理されています。
ランタイムの頭の中を覗いてみよう
V8エンジンの内部メカニズムにおいて、プライベートフィールドは、そのクラスのメソッド(スコープ)からしか参照できない、オブジェクトの隠しスロット(Internal Slots)として保持されます。
イメージ図で表現してみましょう。
[ インスタンスオブジェクト (myAccount) ]
├── owner: “Alice” (パブリック)
└── [[PrivateSlots]] ──> [ #balance: 1500 ] (完全に隔離された空間)
▲
│
[ BankAccount クラスのメソッド群だけがアクセス許可証を持つ ]
外側の世界(グローバルスコープや、クラス外のコード)には、この `[[PrivateSlots]]` のありかを示す「鍵」が存在しません。そのため、どれだけ頑張ってもプロトタイプチェーンを辿っても、`#balance` にたどり着くことは物理的に不可能なのです。
クロージャとの決定的な違い
冒頭で紹介した「クロージャによるカプセル化」と、この「プライベートクラスフィールド」の最大の違いは、メモリの共有効率にあります。
- クロージャの場合: インスタンスを作るたびに、そのプライベート変数を保持するためのスコープ(クロージャ環境)が関数ごとに生成され、メモリ上に保持され続けます。
- プライベートフィールドの場合: メソッド自体はクラスのプロトタイプに一つだけ存在し、インスタンス固有のプライベートデータのみがスロットとしてスマートに保持されます。これにより、V8エンジンの隠しクラス(Hidden Class / Shapes)の最適化が効きやすくなり、メモリ消費量を抑えつつ高速なプロパティアクセスが可能になるのです。
—
まとめ
いかがでしたでしょうか?
プライベートクラスフィールド(`#`)は、単に「コードを綺麗に見せるための新しい書き方」ではありません。
- 外側からの不正なアクセスを構文レベルで完全にシャットアウトする。
- クロージャを使わずに、V8エンジンのメモリ最適化(隠しクラスの恩恵)を維持したまま綺麗にデータを隠蔽する。
この2つを高い次元で両立させた、JavaScriptのモダンな進化形です。
「なぜこの書き方をするのか」「裏側のランタイムで何が起きているのか」を少し意識するだけで、書くコードの質やパフォーマンスへの意識が劇的に変わります。ぜひ、次のモダンなプロジェクトの設計で `#` をガンガン活用してみてくださいね。
あなたのJavaScriptライフが、より一層深みのある楽しいものになりますように!