Q130 - this.reserved を返す getter で、reserved を 20 にしてから読むと?
getter と setter — 読み書きに処理を挟む
プロパティを読んだとき・代入したときに処理を挟む getter と setter を扱います。読むたびに計算する計算プロパティ、代入値の検証、constructor から setter を通す書き方、読み取り専用のプロパティを学べます。
セミナーの申込画面で、残席数を「定員 − 申込数」で表示するとします。インスタンスを作るときに残席数を計算して持たせると、後から申込が増えても表示は変わりません。定員に申込数より少ない値を代入しても、止める場所がありません。
この記事では、プロパティを読んだとき・代入したときに処理を挟む getter と setter を扱います。
読むたびに計算し直す — get と計算プロパティ
残席数を constructor の中で「定員 − 申込数」と計算して持たせると、後から申込数を書き換えても残席数は作ったときの値のままです。メソッドにすれば計算し直せますが、呼ぶ側は定員には括弧を付けず、残席数には付けると覚え分けることになります。
getter(get を付けて class に書き、プロパティとして読まれたときに動くメソッド)は、get remainingSeats() { ... } のように引数なしで書き、return した値が読んだ結果です。
remainingSeats のように、ほかのプロパティから計算して返すプロパティを計算プロパティと呼びます。
class Seminar {
reserved = 0; // 申込数
constructor(capacity) {
this.capacity = capacity; // 定員
}
// 読まれるたびに、その時点の定員と申込数から計算する
get remainingSeats() {
return this.capacity - this.reserved;
}
}
const seminar = new Seminar(30);
console.log(seminar.remainingSeats); // 30(括弧を付けずに読む)
// 申込が 12 件入った後に、もう一度読む
seminar.reserved = 12;
console.log(seminar.remainingSeats); // 18
上の段の 30 は new のときに 1 回だけ計算して保存した値です。下の段の getter は値を保存せず、seminar.remainingSeats と読むたびに return の式を実行するため、reserved を何度書き換えても、残席数を計算し直す行を足す必要はありません。
getter に括弧を付けて呼ぶと TypeError
getter はメソッドとして書きますが、呼ぶ側では括弧を付けません。seminar.remainingSeats() と書くと、返ってきた 18 を関数として呼ぶことになり、TypeError: seminar.remainingSeats is not a function で止まります。
代入される値を確かめる — set とバリデーション
開催が近づくと、会場の都合で定員を 30 から 40 に増やすことがあります。constructor に if と throw で検査を書いても new のときにしか動かないため、作った後に seminar.capacity = 5 と申込数 12 より少ない値を代入すると止まらず、残席数は -7 と表示されます。
setter(set を付けて class に書き、プロパティへの代入で動くメソッド)は、代入された値を引数 value で受け取ります。値そのものは _capacity のような別の名前に入れ、同じ名前の get capacity から返します。
先頭の _ は「外から直接触らない」ことを示す慣習です。
class Seminar {
constructor(capacity, reserved) {
this.reserved = reserved; // 申込数
this._capacity = capacity; // 定員の値そのものは _capacity に入れる
}
get capacity() { return this._capacity; }
set capacity(value) { // 代入された値が value に入る
if (value < this.reserved) {
throw new Error(`定員は申込数 ${this.reserved} 以上です: ${value}`);
}
this._capacity = value;
}
}
const seminar = new Seminar(30, 12);
seminar.capacity = 40; // set capacity が動き、40 を入れる
console.log(seminar.capacity); // 40(get capacity が動く)
seminar.capacity = 5; // Error: 定員は申込数 12 以上です: 5
= の左に書いたときは set が、それ以外で読むときは get が動きます。検査を通らない値は、_capacity に届きません。③ の行は throw で止まるため、try で囲まなければ下の行は実行されません。capacity へ代入する限り setter を通りますが、seminar._capacity = 5 と直接書くと検査を通らずに入ります。これを防ぐ書き方は次の記事で扱います。
setter 内の同名代入は RangeError
set capacity(value) の中で this.capacity = value; と書くと、その代入がまた setter を呼び続け、RangeError: Maximum call stack size exceeded で止まります。値は _capacity のように別の名前に入れます。
作るときも setter を通す — constructor の代入
上の Seminar は constructor が _capacity に直接入れているため、new Seminar(5, 12) は検査を通らずに作られます。constructor にも同じ if を書くと、条件を変えるたびに 2 か所を直すことになり、片方を直し忘れると new と代入で結果が食い違います。
constructor の中で this.capacity = capacity と、_ を付けない名前に代入すると、その 1 行でも setter が動きます。検査を setter の 1 か所に置いたまま、new に渡した値も、作った後に代入した値も、同じ条件で確かめられます。
class Seminar {
constructor(capacity, reserved) {
this.reserved = reserved; // 検査で使う申込数を先に入れる
this.capacity = capacity; // _ の無い名前なので set capacity が動く
}
get capacity() { return this._capacity; }
set capacity(value) {
if (value < this.reserved) {
throw new Error(`定員は申込数 ${this.reserved} 以上です: ${value}`);
}
this._capacity = value;
}
}
try {
new Seminar(5, 12); // new のときも setter の検査を通る
} catch (error) {
console.log(error.message); // 定員は申込数 12 以上です: 5
}
new Seminar(5, 12)— constructor を呼ぶ- setter の例外がここまで戻り、インスタンスは返らない
this.reserved = 12— 検査より先に申込数を入れるthis.capacity = 5—_の無い名前なので set capacity を呼ぶ
valueは 5、this.reservedは 125 < 12なのでthrow—_capacityには入らない
this.capacity = 5 の行が set capacity を呼びます。例外は constructor を抜けて try に届きます。this.reserved = reserved を this.capacity = capacity より下へ移すと、setter の時点で this.reserved は undefined です。undefined は数の比較では NaN として扱われ、5 < NaN は false なので、定員 5 のまま作られます。
代入で書き換えさせない — getter だけのプロパティ
残席数は、定員と申込数から決まる値です。画面側のコードに seminar.remainingSeats = 100 と書けてしまうと、定員 30・申込数 12 なのに残席数 100 と表示され、3 つの値の辻褄が合わなくなります。
getter だけを書き、同じ名前の setter を書かないプロパティは、読み取り専用(読めるが、代入はできない状態)です。class の本体やこの実行環境のような strict mode(一部の誤りを例外にする実行モード)では、代入した行が TypeError で止まります。それ以外では代入が黙って無視されます。
class Seminar {
constructor(capacity, reserved) {
this.capacity = capacity;
this.reserved = reserved;
}
get remainingSeats() { return this.capacity - this.reserved; } // setter は書かない
}
const seminar = new Seminar(30, 12);
try {
seminar.remainingSeats = 100; // set の無い名前への代入
} catch (error) {
console.log(error.name); // TypeError
}
console.log(seminar.remainingSeats); // 18(代入の前のまま)
// 残席数を変えるときは、元になる申込数を書き換える
seminar.reserved = 20;
console.log(seminar.remainingSeats); // 10
下の段で止まるのは、getter だけの名前には代入された値の受け取り先が無いためで、インスタンスに新しいキーも作られません。下の表は、この記事の Seminar で使った書き方と、メソッドを並べたものです。
| 書き方 | 呼ぶ側の書き方 | 向いているもの |
|---|---|---|
| 普通のプロパティ(reserved) | seminar.reserved = 20 でそのまま入る | 検査せずに読み書きしてよい値 |
| getter だけ(remainingSeats) | seminar.remainingSeats で読むだけ | ほかの値から計算して決まる値 |
| getter と setter(capacity) | seminar.capacity = 40 で検査を通る | 代入のたびに確かめたい値 |
| メソッド(例: reserve(count)) | 括弧を付け、引数を渡して呼ぶ | 引数によって結果が変わる処理 |
理解度チェック
まずは1問ずつ答えてみましょう。
Q2set capacity 内で this.capacity = value; と書き、40 を代入すると?
Q3getter だけの seminar.remainingSeats に 100 を代入すると?