こんにちは!フロントエンドからNode.jsの深層まで、JavaScriptの生態系を隅々まで味わってきたフルスタックエンジニアの先輩です。
今回は、JavaScriptのオブジェクト指向とデータ隠蔽(カプセル化)の歴史を塗り替えた、プライベートフィールド(`#`)についてお話しします。
「他の言語(JavaやC++など)では当たり前にある `private` が、なぜJavaScriptでは長年クロージャという裏技を使わなければならなかったのか?」
そして「ES2022で導入された `#` 構文によって、私たちのコードの書き方やV8エンジンのメモリの動きがどう変わったのか?」
ここをクリアすれば、JavaScriptのスコープとクラスの挙動はバッチリマスターできますよ。さあ、一緒に本質の扉を開けていきましょう!
—
1. 昔話:なぜ私たちはクロージャでカプセル化していたのか?
JavaScriptには、長らく「クラスのプロパティを完全に隠す(プライベートにする)」というネイティブな仕組みがありませんでした。すべては `public`(公開)であり、誰からでも書き換え可能な状態でした。
そこで昔の私たちはどうしていたかと言うと、「クロージャ(関数が作られたときのスコープを記憶する仕組み)」を使って、無理やりデータを外から隠していました。
クロージャによるカプセル化の例
function createCounter() {
// この変数は外から直接触れない(スコープで隔離されている)
let count = 0;
return {
increment() {
count++;
console.log(`現在のカウント: ${count}`);
},
getCount() {
return count;
}
};
}
const counter = createCounter();
counter.increment(); // “現在のカウント: 1”
// counter.count としてもアクセスできず、undefined になる
この方法は賢いやり方でしたが、弱点もありました。
クラス構文(`class`)が主流になった現代において、わざわざ関数ファクトリーに戻すのは冗長ですし、何よりインスタンスごとにメソッドがメモリ上に複製されるため、V8エンジンのヒープメモリ効率の観点からも美しくありませんでした。
—
2. 救世主登場:プライベートフィールド(`#`)の基本
ES2022で正式に標準化されたプライベートフィールドは、識別子の先頭に `#` を付けるだけで、そのクラスの外部から絶対にアクセスできない隠しプロパティを作れる機能です。
クロージャのようなトリッキーなハックを使わず、モダンな `class` 構文のままで完全なカプセル化が実現できるようになりました。
プライベートフィールドの基本的な使い方
class BankAccount {
// #を付けることで、クラス外からのアクセスが完全に遮断されます
#balance = 0;
constructor(owner, initialBalance) {
this.owner = owner; // これは公開プロパティ
this.#balance = initialBalance;
}
// 預け入れ(カプセル化されたデータを安全に操作するメソッド)
deposit(amount) {
if (amount <= 0) {
console.log('金額が不正です。');
return;
}
this.#balance += amount;
console.log(`${this.owner}さん、${amount}円預け入れました。残高: ${this.#balance}円`);
}
// 残高確認
getBalance() {
return this.#balance;
}
}
const myAccount = new BankAccount('Taro', 10000);
myAccount.deposit(5000);
// 出力: Taroさん、5000円預け入れました。残高: 15000円
// 外から直接残高を覗き見ようとしてみる
// console.log(myAccount.#balance);
// ❌ 構文エラー (SyntaxError) が発生します!
ここで感動的なのは、`myAccount.#balance` と書いた瞬間に、実行するまでもなく構文エラー(SyntaxError)として弾かれる点です。「うっかり外から機密データを書き換えてバグらせてしまった!」という事故を、JavaScriptのパーサーが未然に防いでくれるわけですね。
—
3. クロージャとプライベートフィールドの決定的な違い
「クロージャでも隠せるなら、どっちでも一緒じゃない?」と思うかもしれませんが、エンジン内部の挙動と設計思想において、両者には決定的な違いがあります。
| 比較項目 | クロージャによるカプセル化 | プライベートフィールド (`#`) |
| :— | :— | :— |
| 実現場所 | 関数スコープの閉包 | クラス構文のネイティブサポート |
| メモリ効率 | インスタンスごとにメソッドが生成されがち | プロトタイプ共有の恩恵を受け、V8で最適化される |
| エラー検知 | 実行時(隠し変数を参照できない等) | コンパイル(パース)時に構文エラーとなる |
| 継承との相性 | カプセル化された変数の共有が難しい | クラス階層ごとに完全にスコープが独立する |
特に注目してほしいのはメモリ効率とV8エンジンの最適化です。
プライベートフィールドは、V8エンジンの隠しクラス(Hidden Class / Shapes)の仕組みに組み込まれており、インスタンスのプロパティ構造が高速に最適化されます。大規模なアプリケーションにおいて、パフォーマンス面でも非常に有利です。
—
4. 初学者がやりがちな「落とし穴」と文法エラー
プライベートフィールドを使う上で、初心者の開発者がよくハマるポイントをいくつか見ておきましょう。ここを知っておくだけで、無駄なハマり時間をゼロにできますよ。
落とし穴 1: 事前宣言が必須であること
通常のパブリックプロパティ(`this.name = ‘taro’` など)は、コンストラクタ内で突然生やすことができますが、プライベートフィールドはクラスのトップレベル(スコープ内)で事前に `#` 付きで宣言しておく必要があります。
class User {
constructor(name) {
// ❌ 宣言せずにコンストラクタ内でいきなり使おうとすると SyntaxError になります
// this.#age = 20;
// ✅ 必ず事前にトップレベルで宣言してください
}
#age = 20; // ここで宣言が必要
}
落とし穴 2: クラスのインスタンス同士なら `#` にアクセスできる
「プライベートなんだから、自分以外のインスタンスの `#` も見えないはず」と思いきや、同じクラスの別のインスタンスであれば、プライベートフィールドにアクセスできてしまいます。 これは他のオブジェクト指向言語(JavaやC++など)と同じ仕様です。
class Team {
#secretCode = ‘JS-Master’;
compareSecret(otherTeam) {
// 同じ Team クラスのインスタンスであれば、他のインスタンスの #secretCode を読める!
return this.#secretCode === otherTeam.#secretCode;
}
}
const teamA = new Team();
const teamB = new Team();
console.log(teamA.compareSecret(teamB)); // true
「クラスの仲間同士(同一設計図から生まれたオブジェクト同士)」であれば、プライベートな秘密を共有できると覚えておくと分かりやすいですね。
—
まとめ:モダンなJavaScript設計を手に入れよう
今回は、プライベートフィールド(`#`)とクロージャによるカプセル化の違いについて深く掘り下げてみました。
- クロージャは、関数スコープを利用した歴史あるカプセル化手法。
- プライベートフィールド(`#`)は、ES2022以降のネイティブかつ高速で安全なカプセル化手法。
現代のJavaScript開発において、クラス内のデータを保護したい場合は、迷わず `#` プライベートフィールドを選択するのがベストプラクティスです。コードの意図が明確になり、エンジンによる最適化の恩恵も受けられます。
この概念をクリアできれば、あなたの書くJavaScriptコードはぐっと洗練され、プロレベルの堅牢性を手に入れられますよ。ぜひ実際のプロジェクトの設計に取り入れてみてくださいね!