みなさん、こんにちは!日々のWeb開発、楽しんでいますか?
JavaScriptを学んでいくと、必ず一度はぶつかる大きな壁があります。それが「カプセル化(データの隠蔽)」と「スコープ」の仕組みです。「クラスの中にある重要なデータを、外部から勝手に書き換えられないように隠したい」というのは、オブジェクト指向プログラミングの基本ですよね。
JavaやC++、TypeScriptなどを触ったことがある方なら `private` というキーワードでおなじみだと思いますが、実はJavaScriptの世界では長い間、「本当の意味でのプライベート変数」が存在しませんでした。
それがES2022でついに導入されたのが、ハッシュマークを頭に付ける「プライベートクラスフィールド(`#`)」です!
この記事では、初学者や他言語から来られた開発者のみなさんに向けて、従来の「クロージャを使った隠蔽」と新しい「`#` による完全隔離」の仕組みを、エンジン内部のメモリ構造のイメージを交えながらわかりやすく解説します。
ここをクリアすれば、JavaScriptのスコープとクラスの基本はバッチリマスターできますよ!一緒にステップバイステップで紐解いていきましょう。
—
1. なぜJavaScriptには「本当のプライベート」が必要だったのか?
まずは歴史を少しだけ振り返ってみましょう。ES2022より前、JavaScript開発者はデータを隠すために2つのアプローチを使っていました。
アプローチ1:アンダースコア(`_`)をつける暗黙の了解
class User {
constructor(name) {
// 「お願いだから外部から触らないでね」という開発者同士のマナー(実際はアクセス可能)
this._secretKey = “XYZ-12345”;
this.name = name;
}
}
const user = new User(“Alice”);
console.log(user._secretKey); // 普通に見えてしまうし、書き換えもできてしまう!
これは単なる「見立て」に過ぎず、言語仕様レベルでの保護はありませんでした。
アプローチ2:クロージャ(Closure)を使った隠蔽
そこで生み出されたテクニックが「クロージャ」です。関数の外側から内部のローカル変数にアクセスできない性質を利用します。
function createBankAccout(initialBalance) {
// この変数は関数スコープの中に隔離される(外からは直接見えない)
let balance = initialBalance;
return {
deposit(amount) {
balance += amount;
return balance;
},
getBalance() {
return balance;
}
};
}
const account = createBankAccout(1000);
console.log(account.getBalance()); // 1000
console.log(account.balance); // undefined(外からは見えない!)
このクロージャを使った方法は長年愛用されてきましたが、クラス構文(`class`)との相性やメモリ効率において大きな問題を抱えていました。
—
2. メモリ構造で見る:「クロージャ」vs「`#` プライベートフィールド」
ここで、V8などのJavaScriptエンジン(ブラウザやNode.jsの心臓部)の中で何が起きているのか、メモリのイメージで比較してみましょう。
1. クロージャ方式のメモリ構造:『バッグを背負ったインスタンス』
クロージャを使ってプライベート変数を実現する場合、メソッド(関数)は各インスタンスが生成されるたびに個別のメモリ領域に再作成されます。
[ インスタンス A ] —> (独自の deposit 関数) —-+
|—> [ 独自のスコープ空間 (balance: 1000) ]
[ インスタンス B ] —> (独自の deposit 関数) —-+—> [ 独自のスコープ空間 (balance: 2000) ]
- メリット: 完全に変数を隠蔽できる。
- デメリット: インスタンスを1,000個作ると、1,000個の関数オブジェクトと1,000個のスコープ環境(Lexical Environment)が作られ、メモリを圧迫します。また、プロトタイプ(`prototype`)による関数の共有ができません。
2. `#`(プライベートフィールド)のメモリ構造:『金庫付きの共通設計図』
ES2022の `#` を使うと、メソッドはプロトタイプ(共通の設計図)に1つだけ置かれ、データだけがインスタンス内部の「特殊な隠しスロット(Internal Slot)」に保管されます。
[ 共通プロトタイプ ] —> (共有 deposit メソッド)
|
v
[ インスタンス A ] —> [#balance: 1000] (隠しスロット)
[ インスタンス B ] —> [#balance: 2000] (隠しスロット)
- メリット: メソッドは全インスタンスで1つだけ共有されるため、メモリ効率が抜群に良い!
- メリット: JavaScriptエンジン(V8など)がクラスの構造をあらかじめ把握できるため、実行速度の最適化(Hidden Classの最適化)が非常に効きやすくなります。
「データはしっかり隠すけれど、処理(メソッド)は無駄なくみんなで共有する」という、最も理想的な形が実現したわけですね。
—
3. プライベートクラスフィールド(`#`)の基本文法
それでは、具体的な書き方を見ていきましょう!使い方はとてもシンプルです。フィールド名の先頭に `#` を付けるだけです。
class BankAccount {
// ① クラスの最上部で「#」付きで宣言する(必須!)
#balance;
constructor(initialBalance) {
// ② 初期値を代入する
this.#balance = initialBalance;
}
// 預金するメソッド
deposit(amount) {
if (amount <= 0) {
throw new Error("預金額は正の数である必要があります");
}
this.#balance += amount;
console.log(`${amount}円預金しました。`);
}
// 残高を確認するゲッター
get balance() {
return this.#balance;
}
}
const myAccount = new BankAccount(5000);
myAccount.deposit(2000); // 出力: 2000円預金しました。
console.log(myAccount.balance); // 出力: 7000 (ゲッター経由で取得)
// ❌ 外部から直接アクセスしようとすると...?
// console.log(myAccount.#balance);
// 構文エラー(SyntaxError: Private field '#balance' must be declared in an enclosing class)
非常にすっきりとしていて読みやすいですよね!
---
4. 初学者が絶対にハマる!3つの罠と文法エラー
`#` は非常に強力ですが、従来のプロパティ(`this.foo`)とは挙動が異なる部分があり、エラーになりやすいポイントが3つあります。事前に知っておけば怖くありませんよ!
罠①:事前にクラス直下で宣言しないと即 `SyntaxError`
普通のプロパティなら、`constructor` の中でいきなり `this.age = 20` と書けば作れますよね。しかし、プライベートフィールドは事前にクラス直下で宣言が必須です。
class Person {
// #age; <-- これを忘れて...
constructor(age) {
// ❌ 宣言していない #age に代入しようとすると文法エラー!
this.#age = age; // SyntaxError!
}
}
罠②:ブラケット記法(文字列での動的アクセス)は使えない
JavaScriptでは `obj[‘name’]` のようにプロパティへアクセスできますが、`#` フィールドにはこれが一切使えません。
class Hero {
#secretIdentity = “Spider-Man”;
getSecret() {
// ❌ 動的にアクセスしようとしても無理!
// return this[‘#secretIdentity’]; // undefined になる(アクセスできない)
// ⭕ 必ずドット記法で明示的に書く必要があります
return this.#secretIdentity;
}
}
なぜなら、`#` によるプライベート化は「文字列のキー」ではなく、JavaScriptがコードを解析する段階(コンパイル時)で厳密に隔離される仕組みだからです。
罠③:`console.log` や JSON 化したときの挙動
`JSON.stringify()` を使ってオブジェクトを文字列に変換するとき、`#` フィールドは完全に無視されます。
class Session {
#token = “SECRET_TOKEN_123”;
user = “Bob”;
}
const session = new Session();
console.log(JSON.stringify(session));
// 出力: {“user”:”Bob”} <-- #token は見事に隠蔽され、外部に出力されません!
Web APIに送信するデータを生成する際、意図せず機密情報が混ざってしまう事故を防げるので、これは嬉しい仕様ですね!
---
5. 実践コード:プライベートメソッドと `in` 演算子によるカプセル化
プライベート化できるのは「変数(フィールド)」だけではありません。「関数(メソッド)」や「ゲッター/セッター」もプライベート化できます。
さらに、ES2022では `in` 演算子を使って「あるオブジェクトが特定のプライベートフィールドを持っているか」を安全にチェックできるようになりました。
以下の実践的なコードを動かしてトレースしてみましょう!
/
- セキュリティ機能付きのユーザー認証管理クラス
/
class Authenticator {
// プライベートフィールドの定義
#passwordHash;
#loginAttempts = 0;
constructor(password) {
// プライベートメソッドを使ってハッシュ化(外部からは呼べない)
this.#passwordHash = this.#hashPassword(password);
}
// 🔒 プライベートメソッド(クラス内部だけで使う秘密の処理)
#hashPassword(password) {
// 簡易的なハッシュ化のシミュレーション
return `hashed_v1_${password.split(”).reverse().join(”)}`;
}
// ログイン検証処理(公開メソッド)
login(inputPassword) {
if (this.#loginAttempts >= 3) {
console.log(“🔒 アカウントがロックされています。サポートに連絡してください。”);
return false;
}
const inputHash = this.#hashPassword(inputPassword);
if (inputHash === this.#passwordHash) {
this.#loginAttempts = 0; // 成功したらリセット
console.log(“✅ ログイン成功!”);
return true;
} else {
this.#loginAttempts++;
console.log(`❌ パスワードが違います。(失敗回数: ${this.#loginAttempts}/3)`);
return false;
}
}
// 💡 ES2022: ‘#フィールド in オブジェクト’ でブランドチェックを行う静的メソッド
static isAuthenticatorInstance(obj) {
// obj が #passwordHash を持っているインスタンスかどうかを安全に判定
return #passwordHash in obj;
}
}
// — 動作確認 —
const auth = new Authenticator(“mySuperSecret123”);
// 1. 正しいパスワードでログイン
auth.login(“mySuperSecret123”); // 出力: ✅ ログイン成功!
// 2. 誤ったパスワードを連続で入力
auth.login(“wrong_pass”); // 出力: ❌ パスワードが違います。(失敗回数: 1/3)
auth.login(“wrong_pass”); // 出力: ❌ パスワードが違います。(失敗回数: 2/3)
auth.login(“wrong_pass”); // 出力: ❌ パスワードが違います。(失敗回数: 3/3)
auth.login(“mySuperSecret123”); // 出力: 🔒 アカウントがロックされています…
// 3. 外部から秘密のメソッドやデータにアクセスしようとする(すべてエラー!)
// auth.#hashPassword(“test”); // SyntaxError!
// auth.#passwordHash = “hacked”; // SyntaxError!
// 4. ブランドチェック(#passwordHash in obj)の確認
console.log(Authenticator.isAuthenticatorInstance(auth)); // 出力: true
console.log(Authenticator.isAuthenticatorInstance({})); // 出力: false
—
まとめ:`#` をマスターして、安全で綺麗なコードを書こう!
今回は、ES2022で登場したプライベートクラスフィールド(`#`)の基本から、従来のクロージャとのメモリ構造の違い、ハマりやすい注意点までを詳しく解説しました。
要点を振り返ってみましょう!
1. 完全な隔離: `#` で始まるフィールドは、クラスの外部から絶対にアクセス・改ざんできない。
2. メモリ効率の向上: クロージャによる隠蔽と違い、メソッドをプロトタイプに配置できるためメモリ消費が最小限で済む。
3. 構文の注意点: クラスの最上部での事前宣言が必須であり、動的アクセス(`this[‘#foo’]`)は不可。
今まで「どうやって外部から変数を守ろう…」と頭を悩ませていた方も、この `#` の仕組みを使えば、シンプルかつ安全に美しいカプセル化を実現できます。
JavaScriptの言語仕様は日々進化していますが、こうした「変数のスコープ」や「メモリの仕組み」という根底の部分を一度理解してしまえば、どんな新しい機能が登場しても怖くありません。
ここをクリアしたあなたなら、モダンJavaScriptのクラス設計はもうバッチリマスターできていますよ!自信を持って、日々のコーディングを楽しんでくださいね。