Q1class Coupon {} のあと、new を付けずに Coupon("SPRING") と呼ぶとどうなりますか?
class の基本 — 定義とインスタンス化
同じ形のデータを何件も作るための class を扱います。constructor と new によるインスタンスの作り方、this で自分の値を読むメソッド、オブジェクトリテラルとの使い分けを学べます。
会員・商品・クーポンのように、同じ項目を持ち同じ計算に使うデータは、アプリの中に何件も現れます。オブジェクトリテラルで 1 件ずつ書くと、項目も計算の式も件数ぶん書き写すことになります。
この記事では、データの形と処理を 1 か所にまとめる class と、そこから new で 1 件ずつ作る書き方を扱います。
データの形を 1 か所で決める — class と new
割引クーポンを 2 種類扱うとします。{ code: "WELCOME", discount: 500 } を件数ぶん書くと、1 件だけ discont と打ち間違えても実行は止まらず、そのクーポンの割引額だけが undefined になります。キー名を書く場所が件数ぶんあるため、打ち間違えがどの 1 件に紛れたかを探すことになります。
class(new で作るオブジェクトの種類を定義する構文)は class Coupon { ... } の中に function なしで constructor(new で呼ばれる部分)を置きます。
その 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
constructor の中で this に書き込まれます。new を付けずに呼ぶと、constructor に入る前に止まります。キー名を書くのは constructor の中の 1 か所だけなので、discont のような打ち間違えがあっても直す場所は 1 か所です。右の列では、コンソールに TypeError: Class constructor Coupon cannot be invoked without 'new' と表示されます。
インスタンスの値で計算する — メソッドと 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 に入るのは apply の左に書いた変数です。引数が同じでも、this が違えば結果が変わります。- 引数
code/discount— この波括弧の中だけで使える this.discount = discount— インスタンスに値を残す
this.discount— インスタンスに残した値を読めるtotal - discount— 名前が見えずReferenceError
discount という名前は constructor の波括弧を出ると見えません。apply からは this に残した値を読みます。apply の中で this. を書き忘れた total - discount は、discount という名前を内側の波括弧から外側へ順に探すため、class の外に同じ名前の変数があるかどうかで結果が変わります。
this. を忘れても止まらないことがある
class の外に const discount = 300; のような同じ名前の変数があると、this. を書き忘れた total - discount は ReferenceError にならず、その 300 を黙って使います。どのクーポンで呼んでも同じ金額が返るときは、this. の書き忘れを疑います。
作った数だけ値を持つ — 複数のインスタンス
クーポンは 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
インスタンスは変数だけでなく配列の要素にも入れられ、coupons[0].use() のように取り出して呼んでも、this はその要素を指します。クーポンが何件あっても、枚数を減らす式は use() の中の 1 行だけです。
作る件数で選ぶ — オブジェクトリテラルとの違い
画面の表示設定のように 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, apply | code, discount |
| instanceof Coupon の結果 | false | true |
| 向いている場面 | 1 回だけ使う設定や応答データ | 同じ形と計算のデータを何件も作る |
class 内のメソッドにカンマを付けない
リテラルを class に書き換えるとき、apply(total) { ... }, のようにメソッドの後ろのカンマを残すと、SyntaxError: Unexpected token ',' が出て 1 行も実行されません。class の波括弧の中は、メソッドを区切り記号なしで並べます。
理解度チェック
まずは1問ずつ答えてみましょう。
Q2外に discount が 300 の変数があり、this. を忘れた welcome.apply(3000) は?
Q3constructor(code, discount) と apply を持つ class のインスタンスに Object.keys を使うと?