こんにちは!フロントエンドからNode.jsの深層まで、JavaScriptのエンジンと日々対話しているチーフアーキテクトです。
今回は、JavaScriptにおける「データ隠蔽(カプセル化)」の歴史を変えた、モダンな言語機能「プライベートクラスフィールド(`#`)」についてお話しします。
「他のオブジェクト指向言語から来たけれど、JSのカプセル化っていまいちスッキリしないな……」
「クロージャを使った隠蔽と何が違うの?」
そんな疑問を持ったことはありませんか?ここをクリアすれば、あなたのJavaScriptのコードは一段と美しく、そしてエンジニアリングとしても堅牢になりますよ。さあ、一緒に本質の世界へ飛び込みましょう!
—
1. そもそもなぜ「隠蔽(カプセル化)」が必要なのか?
プログラムを書いていると、「この変数やメソッドは、クラスの『外』から勝手に触られたくない!」という場面に必ず遭遇しますよね。例えば、銀行口座の残高や、コンポーネントの内部ステートなどです。
外から勝手に書き換えられてしまうと、予期せぬバグ(「なんで残高がマイナスになってるの!?」といった事故)の原因になります。そのため、「見せていい情報(パブリック)」と「隠すべき情報(プライベート)」をきっちり分けること(カプセル化)が、大規模開発では極めて重要になってきます。
JavaScriptでは、長年にわたってこの「隠蔽」を行うためにクロージャという強力な仕組みが使われてきました。まずは、その歴史と、そこにある「メモリ上の秘密」を覗いてみましょう。
—
2. 伝統の技:クロージャによるカプセル化とそのコスト
まずは、モダンな機能が入る前によく使われていた、クロージャによるプライベート変数の再現を見てみましょう。
// クロージャを使った従来のカプセル化の例
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 (外からは見えない!)
この方法は非常に賢く、`balance`という変数を完全に外部から守ることに成功していますよね。
クロージャの抱える「見えないコスト」
しかし、V8エンジンのようなJavaScriptランタイムの裏側を覗くと、ここには少しだけもったいない構造上のコストがあります。
クロージャで変数を隠蔽する場合、関数が呼び出されるたびに、その変数を保持し続けるための「レキシカル環境(スコープチェイン)」がヒープメモリ上に生成されます。もしあなたがこのオブジェクトを何千、何万個と生成した場合、それぞれのインスタンスが独立したスコープの参照を持ち続けるため、メモリ効率の観点やガベージコレクタの追跡コストの面で、決して最適とは言えないのです。
—
3. 革命の到来:プライベートクラスフィールド(`#`)の基本
そこで登場したのが、ECMAScript 2022で正式に導入されたプライベートクラスフィールドです。変数名の頭にシャープ(`#`)を付けるだけで、言語レベルで完全に外部から隔離されたプロパティを作ることができるようになりました。
百聞は一見に如かず、実際の書き方を見てみましょう。
// モダンなプライベートクラスフィールドの例
class BankAccount {
// #を付けることで、クラスの外部から絶対にアクセスできないプライベート変数になる
#balance;
constructor(initialBalance) {
this.#balance = initialBalance;
}
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
}
return this.#balance;
}
getBalance() {
return this.#balance;
}
}
const myAccount = new BankAccount(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)としてビシッと弾かれます。これが、言語レベルでスコープが完全に隔離されているということです。
—
4. ここでやりがち!初心者がハマる「文法エラー」の罠
プライベートクラスフィールドを使い始めの頃、多くの開発者が以下の罠にハマります。ここを知っておくだけで、無駄なデバッグ時間を何時間も節約できますよ。
罠1:事前に `#` の宣言を忘れる
TypeScriptや他の言語の感覚で、コンストラクタ内でいきなり `this.#balance = …` と書きたくなりますが、JavaScriptのクラス構文では、クラスの本体のトップレベルで事前に `#` 付きのフィールドを宣言しておく必要があるケースがほとんどです(※現在の仕様では宣言なしの代入は構文エラーになります)。
class User {
// 🔴 やってしまいがちなエラー:事前の宣言がない
// constructor(name) {
// this.#name = name;
// }
// 🟢 正しい書き方:事前に宣言する
#name;
constructor(name) {
this.#name = name;
}
}
罠2:オブジェクトのコピーやスプレッド構文での見失い
プライベートフィールドは、あくまで「そのクラスのインスタンス内部」だけに結びついています。そのため、スプレッド構文(`{…instance}`)などでオブジェクトを浅いコピー(シャローコピー)しようとすると、プライベートフィールドはコピーされずにごっそり抜け落ちます。
これも、外部から不正にプライベートデータを盗み見られないための、セキュリティ上の重要な仕様なんですよ。
—
5. どちらを使うべき? メモリ効率とカプセル化の結論
最後に、従来の「クロージャ」と「#プライベートフィールド」のどちらを使うべきか、アーキテクトの視点から結論をお伝えします。
- クロージャによる隠蔽:
- 関数型プログラミングのスタイルや、クラスを使いたくない(または使えない)軽量なモジュールパターンを作るときに有効。
- ただし、インスタンス量産時のメモリ効率やパフォーマンス面では一歩譲る。
- プライベートクラスフィールド(`#`):
- クラスベースでコードを書くのであれば、迷わずこちらを選ぶべき。
- V8などのエンジン側もクラス構造に特化した最適化(隠しクラス / Hidden Classes の最適化など)を行えるため、メモリ効率・実行速度の面で非常に有利。
- 何より、コードの意図が「これは絶対に触らせないプライベートだ」と一目でチーム全員に伝わる。
—
まとめ:モダンJSの知見を武器にしよう
いかがでしたでしょうか?
プライベートクラスフィールド(`#`)は、単なる「書き方の好み」ではなく、JavaScriptのランタイムの仕組みやメモリ効率の最適化に深く直結したモダンな機能です。
「クロージャの仕組みも知った上で、クラスでは `#` をスマートに使いこなす」
これができれば、あなたのJavaScriptの基本とアーキテクチャの引き出しは、もうバッチリマスターできていますよ。
日々のコーディングに、ぜひこの強力なカプセル化を取り入れてみてくださいね。それでは、また次の深層でお会いしましょう!