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

constructor とクラスフィールド — 初期値の決め方

インスタンスの初期値を決める constructor とクラスフィールドを扱います。引数の受け取り方、フィールドが入る順番、省いた引数の既定値、作る時点で不正な値を止める書き方を学べます。

問い合わせチケットには、件名のように 1 件ごとに違う値と、対応状況「未対応」のようにどの 1 件も同じ初期値があります。書き方を誤ると、省いた値や不正な値がそのままインスタンスに入ります。

この記事では、作るときの値を受け取る constructor と、決まった初期値を宣言する クラスフィールド を扱います。

引数を整えて持たせる — constructor の本体

問い合わせの受付画面から届く件名には、前後に空白が混ざることがあります。一覧に出す「佐藤 様: ログインできない」のような文字列をインスタンスを作った後で組み立てると、表示する画面の数だけ trim と組み立ての式を書くことになります。

constructor の本体では、this.title = title.trim() のように、this.名前 = の右側に引数を整えた値や、引数を組み合わせた値も書けます。どの画面でもインスタンスのプロパティを読むだけで、整った値を使えます。

class Ticket {
  constructor(title, customerName) {
    // 受け取った値を、整えてから this に入れる
    this.title = title.trim();
    this.customerName = customerName;

    // 表示用の値は、整えた後の this から組み立てる
    this.label = `${this.customerName} 様: ${this.title}`;
    // `${customerName} 様: ${title}` と書くと、件名の空白が残る
  }
}

// 前後に空白の混ざった件名を渡す
const ticket = new Ticket("  ログインできない ", "佐藤");
console.log(ticket.title);    // ログインできない
console.log(ticket.label);    // 佐藤 様: ログインできない
label をどの値から組み立てるか
引数の titleから組み立てるtitle は前後に空白が付いたまま空白ごと埋め込む佐藤 様: の後ろに空白が残るthis.titleから組み立てるthis.title はtrim 済み整えた件名を埋め込む佐藤 様:ログインできない
どちらの段もエラーは出ず、違いは表示にだけ現れます。整えた値を使うには、this に入れた後の値を読みます

上の段は引数の title を、下の段は trim 後の this.title を読んでいます。label の行を constructor の先頭へ移すと、this.titlethis.customerName もまだ代入されていないため、undefined 様: undefined と表示されます。

予約フォームの値から予約データを作ります。selectedRoom、selectedStart、selectedHours は宣言済みです。

① 部屋名・開始時刻・利用時間を持たせる Reservation を定義してください。

② ① の constructor の中で、終了時刻を endHour として持たせてください。

③ 3 つの値を渡して booking を作ってください。

④ 「◯: ◯ 時〜◯ 時(◯ 時間)」の形で表示してください。

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

JavaScript / TypeScript エディタ

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

決まった初期値を並べる — クラスフィールド

受け付けたチケットは、どれも対応状況「未対応」・コメント 0 件から始まります。こうした引数と関係ない初期値を constructorthis.status = "未対応"; と並べると、受け取った値を入れる行と混ざり、インスタンスが持つ値を知るには本体を最後まで読むことになります。

クラスフィールド(class の波括弧の中で、constructor やメソッドの外に書き、new のたびにインスタンスへ入るプロパティ)は、status = "未対応"; のように this を付けず、名前と初期値だけを書きます。本体やメソッドの中から読むときは、ほかのプロパティと同じく this.status と書きます。

class Ticket {
  // 引数と関係なく決まる初期値は、フィールドに並べる
  status = "未対応";
  comments = [];

  constructor(title) {
    this.title = title;
    this.summary = `[${this.status}] ${title}`;   // フィールドの値を読める
  }
}

const first = new Ticket("ログインできない");
const second = new Ticket("請求書が届かない");
console.log(first.summary);                // [未対応] ログインできない

// comments は new のたびに作られた別々の配列
first.comments.push("再起動を依頼しました");
console.log(first.comments.length);        // 1
console.log(second.comments.length);       // 0
new Ticket の中で値が入る順番
new Ticket("ログインできない")① フィールドを上から入れる② 本体を実行this.status を読む③ インスタンスをfirst に入れるインスタンスは空の状態status: "未対応"comments: []title と summaryが加わる4 つのプロパティを持った状態
new の中では、フィールドが入ってから constructor の本体が動きます。本体の中では、フィールドの値をもう読めます

① は new のたびにやり直されるため、comments = [] もインスタンスごとに別の配列になり、first に足したコメントは second には入りません。second.comments.length が 0 のままなのは、このためです。

フィールドに this. を書くと SyntaxError

フィールドを書く位置(constructor の外)に this.status = "未対応"; と書くと、SyntaxError: Unexpected token '.' で 1 行も実行されません。letconst を付けても同じく SyntaxError です。フィールドには名前と初期値だけを書きます。

配送先ごとの出荷データを作ります。destinations は宣言済みです。

① 送料 600 と状態「受付済み」を初期値に持ち、配送先を受け取る Shipment を定義してください。

② ① の constructor で、配送先が「沖縄」のときだけ送料を 1500 に変えてください。

③ 配送先ごとに Shipment を作り、配列 shipments にまとめてください。

④ 1 件ずつ「配送先: 状態 / 送料 ◯ 円」の形で表示してください。

JavaScript / TypeScript エディタ

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

省いたときの値を決める — デフォルト引数

チケットの優先度はほとんどが「通常」で、急ぎのときだけ「至急」を渡したいとします。フィールドに priority = "通常"; と書いたうえで本体に this.priority = priority; を足すと、優先度を省いて作ったチケットの priority は「通常」になりません。

「デフォルト引数と可変長引数」の記事で使ったデフォルト引数(引数が渡されなかったときに使う値を決める書き方)は、constructor にも priority = "通常" の形で書けます。省いた引数や undefined の引数は本体が動く前に既定値へ置き換わるため、this.priority = priority には「通常」が渡ります。

// フィールドに既定値を書き、本体でも引数を代入する
class DraftTicket {
  priority = "通常";
  constructor(title, priority) {
    this.title = title;
    this.priority = priority;   // 右の priority は引数(省くと undefined)
  }
}
console.log(new DraftTicket("ログインできない").priority);     // undefined

// 引数で変えられる値は、デフォルト引数に既定値を書く
class Ticket {
  constructor(title, priority = "通常") {
    this.title = title;
    this.priority = priority;
  }
}
console.log(new Ticket("ログインできない").priority);          // 通常
console.log(new Ticket("請求書が届かない", "至急").priority);   // 至急
フィールドの「通常」が消える順番
new DraftTicket("ログインできない")① フィールドを入れる② 本体でthis.title に代入③ 本体でthis.priority に代入引数 priority はundefinedthis.priority は"通常"this.priority は"通常" のままthis.priority はundefined
フィールドの「通常」は本体より先に入り、本体の代入が引数の undefined で上書きします。最後に残るのは、本体で代入した値です

引数で変えられる値はデフォルト引数に、引数と関係なく決まる値はフィールドに書き分けます。実践 2 の送料のように、受け取った値から条件で決め直す値は、フィールドの初期値を本体の if で書き換えます。下の表は、「至急」を渡したときと、本体で代入しない書き方も含めて、priority に残る値を並べたものです。

書き方件名だけ渡したとき"至急" も渡したとき
フィールドだけ(本体で代入しない)通常通常(渡した値は使われない)
フィールド + 本体で this.priority = priorityundefined至急
デフォルト引数 priority = "通常"通常至急

不正な値では作らせない — constructor で throw

件名が空のチケットや、優先度に「急ぎ」のような決めていない値を持つチケットができると、一覧に空欄が並び、「至急」だけを数える集計からも漏れます。作った後に検査する関数を別に用意しても、呼び忘れた画面では不正なインスタンスがそのまま使われます。

生成時のバリデーション(インスタンスを作る時点で、受け取った値が条件に合うかを確かめること)は、constructor の中に if を書き、合わなければ throw new Error(...) で例外を発生させます。例外が起きると、new の式はインスタンスを返しません。

class Ticket {
  constructor(title, priority = "通常") {
    // 条件に合わない値は、this に入れる前に例外で止める
    if (title.trim() === "") {
      throw new Error("件名が空です");
    }
    if (priority !== "通常" && priority !== "至急") {
      throw new Error(`優先度は 通常 か 至急 です: ${priority}`);
    }
    this.title = title.trim();
    this.priority = priority;
  }
}

try {
  const ticket = new Ticket("ログインできない", "急ぎ");
  console.log(ticket.title);    // この行は実行されない
} catch (error) {
  console.log(error.message);   // 優先度は 通常 か 至急 です: 急ぎ
}
new の中の throw が届く先
try の波括弧(ここで new を呼ぶ)
  • const ticket = new Ticket(...) — 右辺で例外が起き、ticket には何も入らない
  • console.log(ticket.title) — 実行されない
呼び出された constructor の本体
  • 件名の検査 — 空ではないので通る
  • 優先度の検査 — "急ぎ" なので throw する
  • this.title = ... — ここから下は実行されない
catch (error) の波括弧
  • error.message — 「優先度は 通常 か 至急 です: 急ぎ」
例外は new の式を抜け、try の残りを飛ばして catch に届きます。不正なインスタンスは、どの変数にも入りません

検査を constructor に置けば、new Ticket(...) を書く場所がいくつあっても、検査を通らずに作られるチケットはありません。例外の文言に受け取った値を入れておくと、catch の側でどの値が不正だったかも表示できます。

作った後の代入には検査が効かない

constructor の検査が動くのは new のときだけです。作った後で ticket.priority = "急ぎ"; と代入すると、エラーは出ずにそのまま書き換わります。代入のたびに値を確かめる書き方は、getter と setter の記事で扱います。

条件に合う注文明細だけを受け付けます。requests と acceptedLines は宣言済みです。

① 商品名と数量(数量が無ければ 1)を受け取る OrderLine を定義してください。

② 商品名が空なら「商品名が空です」、数量が 1 以上の整数でなければ「数量が不正です: ◯」の例外を発生させてください。

③ 1 件ずつ OrderLine にし、作れたら acceptedLines に入れて「受付: ◯ × ◯」、作れなければ「却下: 例外の文言」を表示してください。

④ 受け付けた件数を「受付件数: ◯」で表示してください。

JavaScript / TypeScript エディタ

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

理解度チェック

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

Q1status = "未対応"; を持つ class の constructorthis.status を読むと?

Q2priority = "通常"; のフィールドだけを持ち、本体で代入しない class に「至急」を渡すと?

Q3try 内の const ticket = new Ticket("") で constructor が throw すると?