private フィールドとアクセサ
このレッスンで分かること
#を先頭に付けたフィールドは、クラスの外から一切触れませんgetとsetは、プロパティに見えるのに中で検査が走る窓口です
直接代入できる値は、いつか壊れる
前のレッスンの Skill は、学習時間をただのプロパティで持っていました。
JavaScript
const skill = new Skill("HTML", 12);
skill.hours = -50;
console.log(skill.hours); // -50学習時間が負になることは現実には起こりませんが、代入は通ってしまいます。おかしな値が入った瞬間には何も起きず、合計や並び替えの場面で初めて表示が崩れます。原因から遠い場所でバグが出るので、探すのに時間がかかります。
「気をつけて代入する」では防げません。半年後の自分も他の人も、書き換えられると知っていれば書き換えます。書き換えられない形にするのが唯一の対策です。
# を付けると外から触れなくなる
フィールド名の先頭に # を付けると、そのクラスの本体の中でしか読み書きできなくなります。
JavaScript
class Skill {
#hours = 0;
constructor(name, hours) {
this.name = name;
this.#hours = hours;
}
get hours() {
return this.#hours;
}
}
const skill = new Skill("HTML", 12);
console.log(skill.hours); // 12
skill.#hours = -50; // SyntaxErrorクラスの本体の外に #hours と書いた時点で構文エラーです。実行時に弾かれるのではなく、そもそも読み込めません。使う前に #hours = 0; とクラスの直下で宣言しておく必要があります。
| 書きかた | 外から読めるか | 外から書けるか | 破られたときに気付けるか |
|---|---|---|---|
this.hours | 読める | 書ける | 気付けない |
this._hours | 読める | 書ける | 気付けない |
this.#hours | 読めない | 書けない | 構文エラーで止まる |
_ を付ける昔ながらの書きかたはただの名前なので、外から skill._hours = -50 と書けば通ります。# は言語の機能なので、約束ではなく仕組みで守られます。
窓口を get と set にする
読むだけなら get、書き込みも許すなら set を足します。
JavaScript
class Skill {
#hours = 0;
set hours(value) {
this.#hours = value > 0 ? value : 0;
}
get hours() {
return this.#hours;
}
get summary() {
return `${this.name} ${this.#hours} 時間`;
}
}呼ぶ側は skill.hours と書くだけで、かっこは付けません。見た目はただのプロパティなのに、読むときは get、書くときは set の中身が動きます。get だけ書いて set を書かなければ読み取り専用になり、代入は黙って無視されます。
summary のように他のフィールドから毎回作れる値は、フィールドとして持ちません。持つと、時間を更新したときに古い文字が残ります。
ただし set で黙って丸めると、呼ぶ側は「入れた値が受け取られたのか、直されたのか」を知る手立てがありません。何度も呼ぶ操作は、成否を返すメソッドにしておきます。
JavaScript
record(minutes) {
if (this.#hours >= 100) {
return false; // 上限なので受け付けない
}
this.#hours += minutes;
return true;
}true か false が返るので、呼ぶ側は弾かれた回数を数えたり、画面に注意書きを出したりできます。中の値は守ったまま、外には十分な情報が渡ります。
全部を
#にすれば安全、ではありません。決まった範囲を外れてはいけない値だけを#にして検査を通し、作ったあと変わらない値は普通のプロパティのままでかまいません。
よくある間違い
#フィールドを宣言せずに使う —constructorの中でいきなりthis.#hours = 0と書いても、クラスの直下に宣言が無ければ構文エラーですsetの中で同じ名前に代入する —set hours(value)の中でthis.hours = valueと書くとset hoursが呼ばれ続け、スタックがあふれます。代入先は#hoursですgetにかっこを付けて呼ぶ —skill.hours()は、返ってきた数値を関数として呼ぼうとしてTypeErrorになります
要件
- class Work は #views を private フィールドとして持つ。constructor は title と initialViews を受け取り、initialViews が 0 以下のときは #views を 0 にする
- get views() で #views を返す。get label() で
タイトル N 回表示の形の文字列を返す - addViews(count) は count が 0 以下なら何もせず false を返し、それ以外は #views に足して true を返す
- function trackWorkViews(title, initialViews, counts) を定義する。counts の各要素を addViews に渡し、{ title, views, label, rejected } を返す。rejected は false が返った回数
入出力例
trackWorkViews("プロフィールサイト", 120, [10,5]) → {"label":"プロフィールサイト 135 回表示","rejected":0,"title":"プロフィールサイト","views":135}
trackWorkViews("自己紹介カード", 0, []) → {"label":"自己紹介カード 0 回表示","rejected":0,"title":"自己紹介カード","views":0}
trackWorkViews("スキル一覧の表", 32, [-5,0,8]) → {"label":"スキル一覧の表 40 回表示","rejected":2,"title":"スキル一覧の表","views":40}
trackWorkViews("学習ログ", -10, [3]) → {"label":"学習ログ 3 回表示","rejected":0,"title":"学習ログ","views":3}