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

getter と setter — 読み書きに処理を挟む

プロパティを読んだとき・代入したときに処理を挟む getter と setter を扱います。読むたびに計算する計算プロパティ、代入値の検証、constructor から setter を通す書き方、読み取り専用のプロパティを学べます。

セミナーの申込画面で、残席数を「定員 − 申込数」で表示するとします。インスタンスを作るときに残席数を計算して持たせると、後から申込が増えても表示は変わりません。定員に申込数より少ない値を代入しても、止める場所がありません。

この記事では、プロパティを読んだとき・代入したときに処理を挟む gettersetter を扱います。

読むたびに計算し直す — get と計算プロパティ

残席数を constructor の中で「定員 − 申込数」と計算して持たせると、後から申込数を書き換えても残席数は作ったときの値のままです。メソッドにすれば計算し直せますが、呼ぶ側は定員には括弧を付けず、残席数には付けると覚え分けることになります。

getterget を付けて 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
残席数を保存するか、読むたびに計算するか
constructor で残席数を保存作った時点の30 - 0 が入るreserved に12 を代入読むと30 のままget で残席数を定義作った時点では何も保存しないreserved に12 を代入読んだ時点の30 - 12 で 18
どちらの段も、読む前に同じ代入をしています。getter は、読んだ時点の reserved で計算します

上の段の 30 は new のときに 1 回だけ計算して保存した値です。下の段の getter は値を保存せず、seminar.remainingSeats と読むたびに return の式を実行するため、reserved を何度書き換えても、残席数を計算し直す行を足す必要はありません。

getter に括弧を付けて呼ぶと TypeError

getter はメソッドとして書きますが、呼ぶ側では括弧を付けません。seminar.remainingSeats() と書くと、返ってきた 18 を関数として呼ぶことになり、TypeError: seminar.remainingSeats is not a function で止まります。

値上げ後も税込価格が古いまま出るメニュー表を、読む側は変えずに直します。MenuItem と menu は宣言済みです。

① constructor で計算する priceWithTax を、読むたびに計算するプロパティに置き換えてください。

② 全商品の税抜価格を 40 円ずつ上げてください。

③ 全商品の「商品名: 税込 ◯ 円」を表示してください。

④ 1 件目の「消費税: ◯ 円」を表示してください。

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

JavaScript / TypeScript エディタ

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

代入される値を確かめる — set とバリデーション

開催が近づくと、会場の都合で定員を 30 から 40 に増やすことがあります。constructorifthrow で検査を書いても new のときにしか動かないため、作った後に seminar.capacity = 5 と申込数 12 より少ない値を代入すると止まらず、残席数は -7 と表示されます。

setterset を付けて 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
代入と読み取りで動くメソッド
get capacityset capacity(value)① seminar.capacity= 40② seminar.capacityを読む③ seminar.capacity= 5set が動き40 は 12 以上get が動き_capacity を返すset が動き5 は 12 未満_capacity が40 になる40 が返るthrow して40 のまま
= の左に書いたときは 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 のように別の名前に入れます。

エアコンの操作パネルの入力を設定温度に反映します。AirConditioner と temperatureInputs は宣言済みです。

① 設定温度を読み書きする temperature を追加し、範囲外なら「設定温度は 16〜30 度です: ◯」の例外を発生させてください。

② 入力を順に代入し、反映できたら「設定温度: ◯ 度」、できなければ例外の文言を表示してください。

③ 最後の設定温度で「部屋名: ◯ 度で運転中」を表示してください。

JavaScript / TypeScript エディタ

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

作るときも setter を通す — constructor の代入

上の Seminarconstructor_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
}
constructor の代入から setter が呼ばれる
try の波括弧(ここで new を呼ぶ)
  • new Seminar(5, 12) — constructor を呼ぶ
  • setter の例外がここまで戻り、インスタンスは返らない
呼び出された constructor の本体
  • this.reserved = 12 — 検査より先に申込数を入れる
  • this.capacity = 5_ の無い名前なので set capacity を呼ぶ
呼び出された set capacity(value) の本体
  • value は 5、this.reserved は 12
  • 5 < 12 なので throw_capacity には入らない
this.capacity = 5 の行が set capacity を呼びます。例外は constructor を抜けて try に届きます

this.reserved = reservedthis.capacity = capacity より下へ移すと、setter の時点で this.reservedundefined です。undefined は数の比較では NaN として扱われ、5 < NaNfalse なので、定員 5 のまま作られます。

社員の氏名を姓と名に分けて登録します。staffNames は宣言済みです。

① 氏名を受け取り、姓と名を別々に持つ Employee を定義してください。

② 「姓 名」で読み書きでき、空白が無ければ「姓と名を空白で区切ってください: ◯」の例外にする fullName を追加してください。

③ 1 人ずつ作り、作れなければ例外の文言を表示してください。

④ 1 人目の fullName に「井上 健太」を代入し、全員の姓・名・氏名を表示してください。

JavaScript / TypeScript エディタ

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

代入で書き換えさせない — 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
残席数への代入が通るか止まるか
残席数を値で保存remainingSeats= 100そのまま100 が入る定員 30・申込 12と合わないget だけで残席数を定義remainingSeats= 100set が無くTypeError30 - 12 の18 のまま
上の段は代入がそのまま通り、定員や申込数と合わない値が残ります。getter だけにすると、代入はその場で止まります

下の段で止まるのは、getter だけの名前には代入された値の受け取り先が無いためで、インスタンスに新しいキーも作られません。下の表は、この記事の Seminar で使った書き方と、メソッドを並べたものです。

書き方呼ぶ側の書き方向いているもの
普通のプロパティ(reserved)seminar.reserved = 20 でそのまま入る検査せずに読み書きしてよい値
getter だけ(remainingSeats)seminar.remainingSeats で読むだけほかの値から計算して決まる値
getter と setter(capacity)seminar.capacity = 40 で検査を通る代入のたびに確かめたい値
メソッド(例: reserve(count))括弧を付け、引数を渡して呼ぶ引数によって結果が変わる処理
QUIZ

理解度チェック

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

Q130 - this.reserved を返す getter で、reserved を 20 にしてから読むと?

Q2set capacity 内で this.capacity = value; と書き、40 を代入すると?

Q3getter だけの seminar.remainingSeats に 100 を代入すると?