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

class の基本 — 定義とインスタンス化

同じ形のデータを何件も作るための class を扱います。constructor と new によるインスタンスの作り方、this で自分の値を読むメソッド、オブジェクトリテラルとの使い分けを学べます。

会員・商品・クーポンのように、同じ項目を持ち同じ計算に使うデータは、アプリの中に何件も現れます。オブジェクトリテラルで 1 件ずつ書くと、項目も計算の式も件数ぶん書き写すことになります。

この記事では、データの形と処理を 1 か所にまとめる class と、そこから new で 1 件ずつ作る書き方を扱います。

データの形を 1 か所で決める — class と new

割引クーポンを 2 種類扱うとします。{ code: "WELCOME", discount: 500 } を件数ぶん書くと、1 件だけ discont と打ち間違えても実行は止まらず、そのクーポンの割引額だけが undefined になります。キー名を書く場所が件数ぶんあるため、打ち間違えがどの 1 件に紛れたかを探すことになります。

classnew で作るオブジェクトの種類を定義する構文)は class Coupon { ... } の中に function なしで constructornew で呼ばれる部分)を置きます。

その this は新しいインスタンス(class から作ったオブジェクト)で、return が無くても new はそれを返します。

// クーポンの形を class で 1 か所に定義する
class Coupon {
  constructor(code, discount) {
    this.code = code;   // 左はインスタンスの code、右は受け取った引数
    this.discount = discount;
  }
}

// new で 1 件ずつ作る
const welcome = new Coupon("WELCOME", 500);
const summer = new Coupon("SUMMER", 1000);
console.log(welcome.code);                // WELCOME
console.log(summer.discount);             // 1000
console.log(welcome instanceof Coupon);   // true

// new を付けずに呼ぶと止まる
// Coupon("SPRING", 300);                 // TypeError
new の有無で分かれる結果
class Couponの定義new Coupon("WELCOME", 500)new Coupon("SUMMER", 1000)Coupon("SPRING", 300)this.code に"WELCOME" を入れるthis.code に"SUMMER" を入れるnew が無いのでconstructor に入らないwelcome にインスタンスが入るsummer に別のインスタンスTypeErrorで止まる渡す渡す
引数は constructor の中で this に書き込まれます。new を付けずに呼ぶと、constructor に入る前に止まります

キー名を書くのは constructor の中の 1 か所だけなので、discont のような打ち間違えがあっても直す場所は 1 か所です。右の列では、コンソールに TypeError: Class constructor Coupon cannot be invoked without 'new' と表示されます。

商品データを class で作ります。productName と productPrice は宣言済みです。

① 商品名と価格を持たせる Product を定義してください。

② 2 つの変数を渡して mouse を作ってください。

③ mouse を「商品名: 価格 円」の形で表示してください。

④ new を付けずに Product を呼ぶ行を try で囲み、エラーの name を表示してください。

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

JavaScript / TypeScript エディタ

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

インスタンスの値で計算する — メソッドと this

割引後の金額を出す式 total - welcome.discount をカート画面と注文確認画面に書くと、計算の決まりが変わるたびに 2 か所を直すことになります。関数にまとめても、項目は class の中、計算は class の外に分かれます。

メソッド(オブジェクトに属し、welcome.apply(3000) のようにドットでつないで呼び出す関数)は、class の波括弧の中に function を付けずに apply(total) { ... } と書きます。メソッドの中の this はドットの左に書いたインスタンスを指し、this.discount は呼んだ時点の値を読みます。

class Coupon {
  constructor(code, discount) {
    this.code = code;
    this.discount = discount;
  }

  // 合計金額から、このクーポンの割引額を引いて返す
  apply(total) {
    return total - this.discount;   // this はドットの左のインスタンス
  }
}

// 2 種類のクーポンを作る
const welcome = new Coupon("WELCOME", 500);
const summer = new Coupon("SUMMER", 1000);

// 同じ apply でも、ドットの左に書いたインスタンスで結果が変わる
console.log(welcome.apply(3000));   // 2500
console.log(summer.apply(3000));    // 2000
ドットの左が this に入る
welcome.apply(3000)this はwelcomethis.discountは 5002500 が返るsummer.apply(3000)this はsummerthis.discountは 10002000 が返る
this に入るのは apply の左に書いた変数です。引数が同じでも、this が違えば結果が変わります
constructor の引数が見える範囲
class Coupon の波括弧
constructor(code, discount) の波括弧
  • 引数 code / discount — この波括弧の中だけで使える
  • this.discount = discount — インスタンスに値を残す
apply(total) の波括弧
  • this.discount — インスタンスに残した値を読める
  • total - discount — 名前が見えず ReferenceError
discount という名前は constructor の波括弧を出ると見えません。apply からは this に残した値を読みます

apply の中で this. を書き忘れた total - discount は、discount という名前を内側の波括弧から外側へ順に探すため、class の外に同じ名前の変数があるかどうかで結果が変わります。

this. を忘れても止まらないことがある

class の外に const discount = 300; のような同じ名前の変数があると、this. を書き忘れた total - discountReferenceError にならず、その 300 を黙って使います。どのクーポンで呼んでも同じ金額が返るときは、this. の書き忘れを疑います。

キーボードの税込価格を、商品が持つメソッドから求めます。Product と keyboard は宣言済みです。

① 税込価格を返すメソッド taxIncluded を Product に追加してください(税率 10%、端数は切り捨て)。

② keyboard の商品名と税込価格を「◯: 税込 ◯ 円」の形で表示してください。

③ keyboard の価格を 9800 に書き換えてください。

④ もう一度 ② と同じ形で表示してください。

JavaScript / TypeScript エディタ

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

作った数だけ値を持つ — 複数のインスタンス

クーポンは 1 枚使うたびに、そのクーポンの残り枚数を 1 減らします。残り枚数を { WELCOME: 3, SUMMER: 3 } のように class の外のオブジェクトで持つと、使う画面ごとに code をキーにして枚数を探し、減らした値を書き戻す式が要ります。

メソッドの中では、this.remaining = this.remaining - 1 のように this のプロパティへ代入もできます。右辺で今の枚数を読み、1 減らした値を同じインスタンスの remaining に書き戻すため、枚数は new で渡した 3 から呼ぶたびに減ります。

class Coupon {
  constructor(code, remaining) {
    this.code = code;
    this.remaining = remaining;   // あと使える枚数
  }

  // ドットの左のインスタンスの枚数を 1 減らす
  use() {
    this.remaining = this.remaining - 1;
  }
}

// 同じ枚数で 2 種類作り、使う回数を変える
const welcome = new Coupon("WELCOME", 3);
const summer = new Coupon("SUMMER", 3);
welcome.use();
welcome.use();
summer.use();
console.log(welcome.remaining);   // 1
console.log(summer.remaining);    // 2
use() で減るのはドットの左だけ
welcome.use()を 2 回this はwelcomeremaining が3 → 2 → 1summer には触れないsummer.use()を 1 回this はsummerremaining が3 → 2welcome には触れない
welcome と summer の use() は、互いの枚数に触れません。減るのは use() の左に書いたインスタンスの枚数です

インスタンスは変数だけでなく配列の要素にも入れられ、coupons[0].use() のように取り出して呼んでも、this はその要素を指します。クーポンが何件あっても、枚数を減らす式は use() の中の 1 行だけです。

倉庫にある 3 商品の在庫を、商品ごとのインスタンスで管理します。出荷用のメソッド ship を持つ Product は宣言済みです。

① モニターアーム 12 個、USB ハブ 30 個、ノート PC スタンド 8 個のインスタンスを作り、配列にまとめてください。

② 1 件目の商品だけを 5 個出荷してください。

③ 全商品の「商品名: 在庫 ◯」を 1 行ずつ表示してください。

④ 在庫の合計を「合計: ◯」の形で表示してください。

JavaScript / TypeScript エディタ

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

作る件数で選ぶ — オブジェクトリテラルとの違い

画面の表示設定のように 1 回だけ使うデータなら、{ ... } をその場で書けば足ります。クーポンのように同じ形と同じ計算を持つデータを何件も作る場面で同じ書き方を続けると、計算の本体まで件数ぶん書き写すことになります。

オブジェクトリテラルにも、キーと値の組と並べて apply(total) { ... } と書けばメソッドを持たせられ、中の this はそのオブジェクトを指します。class と違うのはメソッドの本体を書く場所で、その違いは Object.keys の結果に出ます。

// class: 本体は定義の中に 1 回だけ書く
class Coupon {
  constructor(code, discount) {
    this.code = code;
    this.discount = discount;
  }
  apply(total) { return total - this.discount; }
}
const welcome = new Coupon("WELCOME", 500);

// リテラル: 1 件ごとに本体まで書く
const summer = {
  code: "SUMMER",
  discount: 1000,
  apply(total) { return total - this.discount; },
};

// 持っているキーが違う
console.log(Object.keys(welcome).join(", "));   // code, discount
console.log(Object.keys(summer).join(", "));    // code, discount, apply

本体の場所が違っても、welcome.apply(3000)summer.apply(3000) と同じ形で呼べるため、呼び出す側のコードは変わりません。インスタンスから class の本体を呼び出せる仕組みは、プロトタイプの記事で扱います。下の表は、2 つの書き方の違いを並べたものです。

比べる点オブジェクトリテラルclass
作れる件数{ } を書いたその 1 件new を書くたびに 1 件ずつ
メソッドの本体1 件ごとに書き写すclass の中に 1 回だけ書く
Object.keys に出るキーcode, discount, applycode, discount
instanceof Coupon の結果falsetrue
向いている場面1 回だけ使う設定や応答データ同じ形と計算のデータを何件も作る

class 内のメソッドにカンマを付けない

リテラルを class に書き換えるとき、apply(total) { ... }, のようにメソッドの後ろのカンマを残すと、SyntaxError: Unexpected token ',' が出て 1 行も実行されません。class の波括弧の中は、メソッドを区切り記号なしで並べます。

QUIZ

理解度チェック

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

Q1class Coupon {} のあと、new を付けずに Coupon("SPRING") と呼ぶとどうなりますか?

Q2外に discount が 300 の変数があり、this. を忘れた welcome.apply(3000) は?

Q3constructor(code, discount)apply を持つ class のインスタンスに Object.keys を使うと?