順番に読み進めながら学べます

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.balancethis.#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 に届く行と届かない行
class PrepaidCard の波括弧
  • #balance = 0 — この波括弧の中だけで使える名前
charge(amount) の本体
  • amount > 20000 なら throw して止める
  • 通った金額だけ this.#balance += amount
get balance() の本体
  • return this.#balance — 値を返すだけで書き換えない
class の外の行
  • card.charge(5000) — charge を通して残高を変える
  • card.balance — getter を通して 8000 を読む
  • #balance の名前は、ここには書けない
#balance と書けるのは class の波括弧の中だけです。外の行が残高を変える入口は charge だけです

カプセル化(データとそれを変える処理を class にまとめ、外からの使い方を決めた入口に限る設計)により、この PrepaidCard で残高を書き換える入口は charge、読む入口は getter の balance だけです。

# の名前は宣言しないと SyntaxError

# の名前は、class の本体に #balance = 0;#balance; と宣言してから使います。宣言が無いまま constructorthis.#balance = 0; と代入すると、class の中の行でも SyntaxError で止まります。代入だけでは作られません。

会員のポイント残高を、購入と利用の処理でだけ変えられるようにします。PointAccount と orders は宣言済みです。

① 残高を class の外から読み書きできないフィールドに変えてください。

② 残高を読むだけの points を追加してください。

③ 注文を順に処理し、そのつど残高を表示してください。

④ 元の名前の _points に 999 を代入し、「残高: ◯ pt」を表示してください。

(正しく実行できれば解説が表示されます)

JavaScript / TypeScript エディタ

コードを実行してください

外から書くと実行前に止まる — 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);
外からの代入がどこまで進むか
card._balance= 9999991 行目から順に実行する「動作確認を始めます」が出る代入も通り残高 999999card.#balance= 999999読み込む段階で書き方を確認class の外に# の名前がある実行前に誤りが見つかる
上の段は代入の行まで進み、その前の表示も残ります。下の段は、1 行目を実行する前に止まります

表示が 1 つも出ないまま Private field で始まる SyntaxError になったときは、class の外に書いた # の名前を探します。外から値を変えたい行は、charge のような class のメソッドを呼ぶ形に書き換えます。

店舗の画面で、来店クーポンの残り回数を # の名前に直接足していた行を、class のメソッドを呼ぶ形に直します。VisitCoupon と coupon は宣言済みです。

① クーポンを 2 回使い、「残り ◯ 回」を表示してください。

② 残り回数を増やす addUses を、5 回を超えないように class に追加してください。

③ 2 回足して、残り回数を表示してください。

④ さらに 4 回足して、残り回数を表示してください。

JavaScript / TypeScript エディタ

コードを実行してください

内部の手順を外に出さない — 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)
#withdraw まで進む呼び出し
#withdraw(amount)残高を減らすだけcard.pay(480)card.pay(5000)card.withdraw(5000)480 は残高3000 以下5000 は残高2520 より多い# の無い withdrawという名前は無い#withdraw を通って 2520#withdraw に届かず2520 のままTypeError で止まる
残高を減らす手順に届くのは、確認を通った左の列だけです。# を外した withdraw という名前は、インスタンスにありません

pay(5000) は残高の確認で return するため、#withdraw まで進みません。外に card.#withdraw(5000) と書くと #balance と同じく読み込む段階で SyntaxError になり、右の列の TypeError# の無い別の名前を呼んだ結果です。

private メソッドは this. を付けて呼ぶ

class の中でも、this. を省いて #withdraw(price); と書くと、実行前に SyntaxError: Unexpected identifier '#withdraw' で止まります。private フィールドと同じく、this.#withdraw(price) の形で呼びます。

レンタル自転車の今の料金と延長料金を、同じ計算で求めます。BikeRental と rides は宣言済みです。

① 料金を求める calcFee を、外から呼べないメソッドで追加してください。

② ① を使い、料金を読むプロパティ fee と、延長分の追加料金を返す extraFee を追加してください。

③ 全件の「◯ 分: ◯ 円(10 分延長で +◯ 円)」を表示してください。

④ 1 件目の calcFee を外から読み、型を表示してください。

JavaScript / TypeScript エディタ

コードを実行してください

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}
JSON.stringify が読む枠
prepaidCard(#balance を持つインスタンス)
プロパティのキー
  • キーは 1 つも無い(get balance() は class に書いたメソッド)
  • JSON.stringify(prepaidCard) の結果は {}
private フィールド
  • #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メソッドなので出ない
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1console.log の後に、class の外で card.#balance = 0; と書いたコードを実行すると?

Q2#withdraw を持つ card で、外から card.withdraw(500) と呼ぶと?

Q3#balance = 3000 と getter だけの cardJSON.stringify にすると?