Q1console.log の後に、class の外で card.#balance = 0; と書いたコードを実行すると?
private フィールド (#) とカプセル化
名前の先頭に # を付けて、class の外から読み書きできなくする private フィールドと private メソッドを扱います。外から書いたときに実行前に出る SyntaxError、慣習の _ との違いを学べます。
プリペイドカードの残高を _balance に入れても、画面側のコードに card._balance = 999999 と書けば、そのまま入ります。先頭の _ は外から触らないことを示す慣習で、代入も呼び出しも止めません。
この記事では、外から読み書きできない private フィールド と private メソッド を扱います。
残高を class の中に隠す — private フィールド
残高を変えてよいのは、1 回 20000 円までの上限を確かめる charge を通ったときだけにしたいとします。_balance のままでは、返金の画面を作った別のコードが card._balance += 30000 と書いても止まらず、上限を超えた金額が残高に入ります。
private フィールド(先頭に # を付けて class の中で宣言し、その class の波括弧の中からだけ読み書きできるフィールド)は、#balance = 0; と宣言し、this.#balance と書いて使います。
# も名前の一部なので、this.balance と this.#balance は別の名前です。
class PrepaidCard {
#balance = 0; // 残高。class の外からは読み書きできない
charge(amount) {
if (amount > 20000) {
throw new Error(`1 回のチャージは 20000 円までです: ${amount}`);
}
this.#balance += amount; // class の中なので書き換えられる
}
get balance() { return this.#balance; } // 外には読むための getter だけを出す
}
const card = new PrepaidCard();
card.charge(5000); // 残高を変える入口は charge だけ
card.charge(3000);
console.log(card.balance); // 8000
#balance = 0— この波括弧の中だけで使える名前
amount > 20000ならthrowして止める- 通った金額だけ
this.#balance += amount
return this.#balance— 値を返すだけで書き換えない
card.charge(5000)— charge を通して残高を変えるcard.balance— getter を通して 8000 を読む#balanceの名前は、ここには書けない
#balance と書けるのは class の波括弧の中だけです。外の行が残高を変える入口は charge だけです。カプセル化(データとそれを変える処理を class にまとめ、外からの使い方を決めた入口に限る設計)により、この PrepaidCard で残高を書き換える入口は charge、読む入口は getter の balance だけです。
# の名前は宣言しないと SyntaxError
# の名前は、class の本体に #balance = 0; や #balance; と宣言してから使います。宣言が無いまま constructor で this.#balance = 0; と代入すると、class の中の行でも SyntaxError で止まります。代入だけでは作られません。
外から書くと実行前に止まる — SyntaxError
動作確認のために、画面側のコードで残高を 999999 にしたいとします。_balance のころと同じつもりで card.#balance = 999999 と書くと、この行は通りません。しかも、その行より上にあって先に表示されるはずの行まで表示されなくなります。
class の外で card.#balance と書くと、let の再宣言と同じくコードを読み込む段階で SyntaxError: Private field '#balance' must be declared in an enclosing class が出ます。「private フィールドは、囲んでいる class の中で宣言する」という意味です。
class PrepaidCard {
#balance = 0;
charge(amount) { this.#balance += amount; }
get balance() { return this.#balance; }
}
console.log("動作確認を始めます"); // この行も表示されない
const card = new PrepaidCard();
card.charge(5000);
// _balance なら上から順に実行されるが、# の名前は読み込む段階で止まる
card.#balance = 999999; // SyntaxError: Private field '#balance' must be declared in an enclosing class
console.log(card.balance);
表示が 1 つも出ないまま Private field で始まる SyntaxError になったときは、class の外に書いた # の名前を探します。外から値を変えたい行は、charge のような class のメソッドを呼ぶ形に書き換えます。
内部の手順を外に出さない — private メソッド
残高を減らす処理を、pay から withdraw という通常のメソッドに切り出したとします。すると class の外からも card.withdraw(5000) と呼べるため、pay の中の残高の確認を飛ばして、残高をマイナスにできます。
private メソッド(先頭に # を付けて定義し、同じ class のメソッドの中から this.#withdraw(price) の形でだけ呼べるメソッド)にすると、切り出した手順は外から呼べません。後から名前や引数を変えても、直す範囲は class の中に収まります。
class PrepaidCard {
#balance = 3000;
// 残高を減らすだけの手順。足りるかどうかは pay が先に確かめる
#withdraw(amount) { this.#balance -= amount; }
pay(price) {
if (price > this.#balance) {
return `残高が足りません: ${price}`;
}
this.#withdraw(price); // class の中なので呼べる
return `支払い後の残高: ${this.#balance}`;
}
}
const card = new PrepaidCard();
console.log(card.pay(480)); // 支払い後の残高: 2520
console.log(card.pay(5000)); // 残高が足りません: 5000
console.log(typeof card.withdraw); // undefined(呼ぶと TypeError)
pay(5000) は残高の確認で return するため、#withdraw まで進みません。外に card.#withdraw(5000) と書くと #balance と同じく読み込む段階で SyntaxError になり、右の列の TypeError は # の無い別の名前を呼んだ結果です。
private メソッドは this. を付けて呼ぶ
class の中でも、this. を省いて #withdraw(price); と書くと、実行前に SyntaxError: Unexpected identifier '#withdraw' で止まります。private フィールドと同じく、this.#withdraw(price) の形で呼びます。
JSON に残る値を見分ける — _ と # の違い
_balance で持っていたプリペイドカードの class を、#balance に書き換えるとします。外からの代入は止められますが、インスタンスを JSON.stringify で文字列にして保存していた場合、書き換えた後の保存データからは残高が消えます。
_ を付けた名前は _ で始まるだけの普通のプロパティなので、JSON.stringify に出ます。# のフィールドはプロパティのキーに並ばないため結果に含まれず、getter も class に書いたメソッドでキーには並ばないので、balance も出ません。
// 書き換える前: _ を付けた普通のプロパティ
class LegacyPrepaidCard {
constructor() { this._balance = 3000; }
}
// 書き換えた後: # を付けた private フィールドと、読むための getter
class PrepaidCard {
#balance = 3000;
get balance() { return this.#balance; }
}
const legacyCard = new LegacyPrepaidCard();
const prepaidCard = new PrepaidCard();
console.log(JSON.stringify(legacyCard)); // {"_balance":3000}
console.log(JSON.stringify(prepaidCard)); // {}
console.log(JSON.stringify({ balance: prepaidCard.balance })); // {"balance":3000}
- キーは 1 つも無い(
get balance()は class に書いたメソッド) JSON.stringify(prepaidCard)の結果は{}
#balance— 3000- PrepaidCard の中の行だけが読める
#balance はインスタンスにありますが、プロパティのキーの枠の外です。JSON.stringify が読むのは、キーの枠の中だけです。{} になっても残高は消えておらず、prepaidCard.balance で 3000 を読めます。保存するときは、サンプルの最後の行のように getter で値を取り出したオブジェクトを渡します。下の表は、この記事で使った 3 つの書き方を並べたものです。
| 書き方 | class の外から書いたとき | JSON.stringify の結果 |
|---|---|---|
| _ を付けた名前(_balance) | 読み書きでき、検査を通らずに入る | {"_balance":3000} のように出る |
| private フィールド(#balance) | 読み込む段階で SyntaxError | 出ない({} になる) |
| private メソッド(#withdraw) | card.#withdraw() は SyntaxError、card.withdraw() は TypeError | メソッドなので出ない |
理解度チェック
まずは1問ずつ答えてみましょう。
Q2#withdraw を持つ card で、外から card.withdraw(500) と呼ぶと?
Q3#balance = 3000 と getter だけの card を JSON.stringify にすると?