こんにちは!フロントエンドからNode.jsの深部まで、JavaScriptの生態系を隅々まで愛するチーフアーキテクトの先輩です。
今回は、JavaScriptのクラス構文における「プライベートクラスフィールド(`#`)」についてお話ししますね。
「オブジェクト指向プログラミングでデータを隠蔽したい!」と思ったとき、これまではクロージャや命名規則(アンダースコア `_`)に頼るのが常道でした。しかし、モダンなJavaScriptには、言語仕様レベルでデータを完全に隔離する強力な武器が備わっています。
ここをクリアすれば、オブジェクトのデータ管理とカプセル化の概念はバッチリマスターできますよ。一緒に本質を深く掘り下げていきましょう!
—
1. なぜ「プライベート変数」が必要なのか?
プログラムの世界では、「外から触られたくない大切なデータ」と「外から自由に使っていい機能(メソッド)」を明確に分けることが、安全で保守しやすいコードを作る大原則です。これをカプセル化(データ隠蔽)と呼びます。
例えば、銀行口座をイメージしてください。残高の金額(データ)を、街行く人が勝手に書き換えられたら大問題ですよね。「預金を引き出す」「残高を見る」という決まった手続き(メソッド)を通した時だけ、内部の残高が変わるように保護しなければなりません。
JavaScriptでも同じです。クラスの内部状態が外から無防備に書き換えられる状態を放置すると、予期せぬバグの温床になります。この課題を解決するために進化してきた歴史を見ていきましょう。
—
2. 伝統的手法:クロージャによるカプセル化の限界
ES2015(ES6)で `class` 構文が導入される前、あるいはクラス内でも真のプライベートを実現するために、私たちはクロージャ(関数が作られたときのスコープを記憶する仕組み)を使っていました。
クロージャを使った実装例
function createBankAccount(initialBalance) {
// この変数 ‘balance’ は、外の世界からは直接アクセスできない「プライベート変数」
let balance = initialBalance;
return {
deposit(amount) {
if (amount > 0) {
balance += amount;
}
return balance;
},
getBalance() {
return balance;
}
};
}
const myAccount = createBankAccount(1000);
console.log(myAccount.deposit(500)); // 1500
console.log(myAccount.balance); // undefined (外からは見えない!)
この方法は非常に賢く機能しますが、いくつかのコスト(代償)がありました。
1. メモリの非効率性: クラスのように「設計図から `new` で量産する」構造ではなく、関数を呼ぶたびにメソッド(関数オブジェクト)がメモリ上に新しく生成されるため、インスタンスが増えるほどメモリを圧迫します。
2. 構文の冗長性: 「クラス」という直感的なキーワードを使えず、ファクトリー関数(オブジェクトを返す関数)のパターンに頼る必要がありました。
—
3. 現代の答え:プライベートクラスフィールド(`#`)の基本
そこで登場したのが、プレフィックスとして `#` を変数名の頭につけるプライベートクラスフィールドです。V8などのモダンなJavaScriptエンジンは、これを言語仕様(構文)として完全にサポートし、クラスのインスタンスごとに安全で高速な隠蔽領域を提供しています。
百聞は一見にしかず、実際のコードを見てみましょう。
`#` を使ったモダンなクラス設計
class BankAccount {
// プライベートフィールドの宣言(クラスの最上位で定義する必要があります)
#balance;
constructor(initialBalance) {
// クラス内であれば、#を付けてアクセス可能
this.#balance = initialBalance;
}
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
}
return this.#balance;
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount(1000);
console.log(account.deposit(500)); // 1500
// console.log(account.#balance); // ❌ SyntaxError!外からは絶対にアクセスできません
外から `account.#balance` と書こうものなら、JavaScriptパーサーが即座に文法エラーを吐き出します。命名規則としての `_balance`(アンダースコア付き変数)とは異なり、「破ることができない物理的な壁」がそこに存在しているのです。
—
4. 陥りやすい文法エラーと注意すべきポイント
プライベートフィールドは非常に強力ですが、初学者が思わずハマりがちな「落とし穴」がいくつかあります。ここを押さえておくと、デバッグで無駄な時間を使わずに済みますよ。
注意点その1:事前にフィールドの宣言が必要
従来のJavaScript(クラスプロパティが提案される前)では、コンストラクタ内でいきなり `this.foo = 123` と書くことができました。しかし、プライベートフィールド(`#`)は、クラスの本体の直下(コンストラクタの外)であらかじめ宣言しておく必要があります。
class User {
// #name; // 👈 これを書き忘れるとエラーになります!
constructor(name) {
this.#name = name; // 宣言なしでここに書くと SyntaxError
}
}
※もし初期値がない場合でも、`#name;` のように空で宣言しておく必要があります。
注意点その2:オブジェクトのキーとして動的に追加できない
通常のプロパティであれば `account[‘_balance’]` のようにブラケット記法で動的にアクセスできますが、プライベートフィールドは静的な構文解析に依存しているため、以下のようなアクセスはできません。
const fieldName = ‘#balance’;
// account[fieldName]; ❌ これは意図通りに動きません(プロパティ名としての文字列 `#balance` を探してしまいます)
—
5. 内部の仕組み:ランタイム(V8エンジン)の視点
ここで少しだけ、ブラウザやNode.jsの裏側で動いているJavaScriptエンジン(V8など)の視点に降りてみましょう。
「なぜ `#` のプライベート変数はクロージャよりも安全で、かつ高速なのか?」
V8エンジンは、オブジェクトの構造(プロパティの並び順やメモリ上のオフセット)を効率的に管理するために「Hidden Class(隠れクラス)」という最適化の仕組みを持っています。
従来の `_balance` のようなパブリックなプロパティは、オブジェクトが後から動的に変更される可能性を考慮して管理されますが、`#` で宣言されたプライベートフィールドは、「このクラスのインスタンスには必ずこの隠しスロットが存在する」ことがコンパイル時に確定します。
そのため、エンジンはメモリ上の固定されたオフセット位置へダイレクトにアクセスできます。つまり、言語レベルの安全性を手に入れた上に、実行パフォーマンスの最適化の恩恵もフルに受けられるという、アーキテクツにとって夢のような仕組みになっているのです。
—
まとめ
いかがでしたでしょうか?
- クロージャによるカプセル化は歴史ある手法ですが、メモリ効率や構文の面でスマートではありませんでした。
- プライベートクラスフィールド(`#`)は、ランタイムの最適化を損なうことなく、真のデータ隠蔽をシンプルに実現するモダンJSの標準機能です。
「データを隠し、必要な窓口(メソッド)だけを公開する」。このオブジェクト指向の基本原則を、`#` を使ってぜひあなたのコードに取り入れてみてください。
ここをマスターすれば、あなたの書くJavaScriptコードの安全性と美しさは一段と跳ね上がりますよ。次のステップも一緒に楽しく学んでいきましょう!