JavaScript / LESSON 14 / JS14

JavaScript:クラスと継承

1学習の目的

2基礎解説

やりたいこと書き方備考
設計図を作るclass Item { }名前は大文字始まり
初期化するconstructor(name) { }new のとき自動で呼ばれる
計算値を持つget subtotal() { }item.subtotal(カッコ不要)
継承するclass Book extends Item { }super() で親を呼ぶ
✅ 覚えるべき重要ポイント
  1. クラスは「設計図」、インスタンスは「実物」new で作った実物はそれぞれ独立していて、片方を変えても他方に影響しない。
  2. thisその実物自身を指す。this.name でそのインスタンスのデータを読み書きする。
  3. get を付けるとプロパティのように呼べるitem.subtotal、カッコなし)。計算して求まる値に使うと自然に書ける。
  4. 子クラスで constructor を書いたら必ず先頭で super(…) を呼ぶ。呼ばずに this を使うと ReferenceError になる。
  5. # を付けたフィールドはクラスの外から一切触れない(プライベート)。書き換えられたくない状態を守れる。

現場使用例:商品・ユーザー・注文などデータのまとまり、残高のような状態を持つもの、APIクライアント、Error を継承した自作エラー(JS10)。「名詞」で表せるものはクラスの候補

class Item {
  constructor(name, price, qty = 1) {
    this.name = name;
    this.price = price;
    this.qty = qty;
  }

  get subtotal() {            // カッコなしで呼べる
    return this.price * this.qty;
  }

  toString() {                // 文字列にするとき自動で呼ばれる
    return `${this.name}:${this.subtotal.toLocaleString()}円`;
  }
}

class PhysicalItem extends Item {
  constructor(name, price, qty, weight) {
    super(name, price, qty);   // 親の初期化を先に呼ぶ
    this.weight = weight;
  }

  get subtotal() {
    return super.subtotal + 500;   // 親の計算に送料を足す
  }
}

const a = new Item("eBook", 2400, 2);
const b = new PhysicalItem("机", 20000, 1, 10);

console.log(`${a}`);          // eBook:4,800円
console.log(`${b}`);          // 机:20,500円
console.log(b instanceof Item); // true

3基本ドリル(10問)

JS14-D01 名前を持つクラス Person を定義し、new Person("田中") を作ってその名前を出力せよ。 コーディング★☆☆無料
期待される結果

出力 == 田中

ヒント

constructor(name) の中で this.name = name とする。取り出しは p.name。

模範解答
class Person {
  constructor(name) {
    this.name = name;
  }
}

const p = new Person("田中");
console.log(p.name);
解説

クラスの最小形。constructor で受け取った値を this に入れておくと、あとからプロパティとして取り出せる。

JS14-D02 new でインスタンスを作ったときに自動で呼ばれるメソッドはどれか。
A. init B. constructor C. create D. new
選択★☆☆無料
期待される結果

解答 == B

ヒント

「構築する」という意味の英単語。

模範解答
B
解説

省略もできて、その場合は何もしない constructor があるものとして扱われる。初期値を設定したいときに書く

JS14-D03 半径を持つ Circle クラスを定義し、面積を返すメソッド area を作れ。new Circle(10) の面積を出力せよ(円周率は3.14)。 コーディング★☆☆無料
期待される結果

出力 == 314

ヒント

メソッドの中では this.r で半径を参照する。面積は this.r ** 2 * 3.14。

模範解答
class Circle {
  constructor(r) {
    this.r = r;
  }

  area() {
    return this.r ** 2 * 3.14;
  }
}

console.log(new Circle(10).area());
解説

メソッドは引数を渡さなくても自分のデータを使える。関数なら area(10) と半径を毎回渡す必要がある。

JS14-D04 インスタンス自身を指すキーワードを補ってコードを完成させよ。 穴埋め★☆☆無料
コード
class Dog {
  constructor(name) {
    ____.name = name;
  }
}

console.log(new Dog("ポチ").name);
期待される結果

出力 == ポチ

ヒント

「これ自身」を意味する4文字のキーワード。

模範解答
this
解説

thisそのとき操作している実物を指す。100個インスタンスを作れば、それぞれの this が別のオブジェクトを指す。

JS14-D05 get subtotal() { … } と定義したとき、呼び出す正しい書き方はどれか。
A. item.subtotal() B. item.subtotal C. item.get.subtotal D. get(item.subtotal)
選択★☆☆無料
期待される結果

解答 == B

ヒント

getter はプロパティのように振る舞う。

模範解答
B
解説

カッコを付けないのが getter の特徴。付けると関数そのものを呼ぼうとしてエラーになる。「計算して求まる値」をプロパティらしく見せられる。

JS14-D06 残高を持つ Account クラスを定義し、入金するメソッド deposit を作れ。残高1000で作成して500入金し、最終残高を出力せよ。 コーディング★★☆無料
期待される結果

出力 == 1500

ヒント

deposit の中で this.balance += amount とすればプロパティを書き換えられる。

模範解答
class Account {
  constructor(balance) {
    this.balance = balance;
  }

  deposit(amount) {
    this.balance += amount;
  }
}

const a = new Account(1000);
a.deposit(500);
console.log(a.balance);
解説

メソッドで自分の状態を変えるのがクラスの本領。関数だと変更後の値を return して受け取り直す必要がある。

JS14-D07 親クラスの初期化を呼び出すコードを完成させよ。 穴埋め★★☆無料
コード
class Animal {
  constructor(name) {
    this.name = name;
  }
}

class Cat extends Animal {
  constructor(name, indoor) {
    ____(name);
    this.indoor = indoor;
  }
}

console.log(new Cat("タマ", true).name);
期待される結果

出力 == タマ

ヒント

「親(スーパークラス)」を表すキーワード。関数のように呼ぶ。

模範解答
super
解説

super() より前に this を使うとエラーになる。子で constructor を書いたら、まず super() を呼ぶと機械的に覚える。

JS14-D08 #count のように # で始まるフィールドはどうなるか。
A. 定数になる B. クラスの外から触れなくなる C. 静的メンバになる D. 自動で初期化される
選択★★☆無料
期待される結果

解答 == B

ヒント

「プライベート」を表す記号。

模範解答
B
解説

外から obj.#count と書くと構文エラーになる。触ってほしくない状態を確実に守れるので、残高やトークンなどに使う。

JS14-D09 単価と数量を持つ Item クラスに、小計を返す getter subtotal を定義せよ。new Item(1500, 3) の小計を出力すること。 コーディング★★☆無料
期待される結果

出力 == 4500

ヒント

get subtotal() { return this.price * this.qty; } と書き、item.subtotal で呼ぶ。

模範解答
class Item {
  constructor(price, qty) {
    this.price = price;
    this.qty = qty;
  }

  get subtotal() {
    return this.price * this.qty;
  }
}

console.log(new Item(1500, 3).subtotal);
解説

常に最新の値が計算されるのが getter の利点。qty を変えれば subtotal も自動で変わるので、値がズレる心配がない。

JS14-D10 Animal クラスに speak("..." を返す)を定義し、それを継承した Dog で speak を "ワン" に上書きせよ。new Dog().speak() の結果を出力すること。 コーディング★★☆無料
期待される結果

出力 == ワン

ヒント

class Dog extends Animal と書き、中で同名の speak を定義すれば上書きされる。

模範解答
class Animal {
  speak() {
    return "...";
  }
}

class Dog extends Animal {
  speak() {
    return "ワン";
  }
}

console.log(new Dog().speak());
解説

オーバーライドの最小形。親と同じ名前で定義するだけで子が優先される。親の処理も使いたければ super.speak() で呼べる。

4実践シナリオ(5問)

JS14-S01 【商品クラスで請求明細】商品名・単価・数量を持つ Item クラスに、getter subtotal と toString(「ノートPC:128,000円 × 1 = 128,000円」の形式)を定義せよ。ノートPC(128000,1)・マウス(3200,2)・USBメモリ(1500,3) の3件を出力し、最後に「合計:138,900円」と出力すること。 コーディング★★☆無料
期待される結果

出力4行が完全一致

ヒント

テンプレートリテラルに ${item} と書くと toString が自動で呼ばれる。合計は reduce で。

模範解答
class Item {
  constructor(name, price, qty) {
    this.name = name;
    this.price = price;
    this.qty = qty;
  }

  get subtotal() {
    return this.price * this.qty;
  }

  toString() {
    return `${this.name}:${this.price.toLocaleString()}円 × ${this.qty} = ${this.subtotal.toLocaleString()}円`;
  }
}

const items = [
  new Item("ノートPC", 128000, 1),
  new Item("マウス", 3200, 2),
  new Item("USBメモリ", 1500, 3),
];

for (const item of items) {
  console.log(`${item}`);
}

const total = items.reduce((sum, i) => sum + i.subtotal, 0);
console.log(`合計:${total.toLocaleString()}円`);
解説

JS08・JS13と同じ請求書だが、表示形式がクラスの中に閉じ込められた。明細の見た目を変えたければ toString だけを直せばよい。

JS14-S02 【学生クラス】名前と点数配列を持つ Student クラスに、平均を返す getter average と、平均60以上なら「合格」を返す getter judge を定義せよ。田中[82, 91, 67, 76] と鈴木[45, 60, 38, 67] について「田中:平均79.0点 → 合格」の形式で2行出力すること。 コーディング★★☆無料
期待される結果

出力2行が完全一致

ヒント

judge の中で this.average を参照できる(カッコ不要)。平均は toFixed(1)。

模範解答
class Student {
  constructor(name, scores) {
    this.name = name;
    this.scores = scores;
  }

  get average() {
    return this.scores.reduce((s, n) => s + n, 0) / this.scores.length;
  }

  get judge() {
    return this.average >= 60 ? "合格" : "不合格";
  }
}

const students = [
  new Student("田中", [82, 91, 67, 76]),
  new Student("鈴木", [45, 60, 38, 67]),
];

for (const s of students) {
  console.log(`${s.name}:平均${s.average.toFixed(1)}点 → ${s.judge}`);
}
解説

getter から別の getter を呼べる。judge は average に依存しているが、点数を変えれば両方とも自動で追随するのでズレようがない。

JS14-S03 【銀行口座を守る】残高を #balance でプライベートに持つ Account クラスを定義せよ。deposit(入金)、withdraw(出金・残高不足なら throw new Error("残高が不足しています"))、getter balance を用意すること。
残高10,000で作り、5,000入金 → 3,000出金 → 999,999出金(失敗)を行い、「入金後:15,000円」「出金後:12,000円」「エラー:残高が不足しています」「最終残高:12,000円」の4行を出力せよ。
コーディング★★☆無料
期待される結果

出力4行が完全一致

ヒント

#balance はクラスの中でのみ参照できる。失敗する出金は try / catch で受け止める(JS10)。

模範解答
class Account {
  #balance;

  constructor(balance = 0) {
    this.#balance = balance;
  }

  get balance() {
    return this.#balance;
  }

  deposit(amount) {
    this.#balance += amount;
  }

  withdraw(amount) {
    if (amount > this.#balance) {
      throw new Error("残高が不足しています");
    }
    this.#balance -= amount;
  }
}

const account = new Account(10000);

account.deposit(5000);
console.log(`入金後:${account.balance.toLocaleString()}円`);

account.withdraw(3000);
console.log(`出金後:${account.balance.toLocaleString()}円`);

try {
  account.withdraw(999999);
} catch (e) {
  console.log(`エラー:${e.message}`);
}

console.log(`最終残高:${account.balance.toLocaleString()}円`);
解説

残高を直接書き換える手段が存在しないのが要点。account.balance = 999999 と書いても getter しかないので効かない。不正な操作をクラス自身が拒否する設計になっている。

JS14-S04 【会員種別で割引を変える】Member クラス(name・kind「一般」・rate 0)に、支払額を返す finalPrice(price) と toString(「田中(一般)」形式)を持たせよ。Premium(「プレミアム」・0.1)と VIP(「VIP」・0.2)を継承で定義すること。
田中(一般)・鈴木(プレミアム)・佐藤(VIP)の3人について、10,000円の商品の支払額を「田中(一般):10,000円」の形式で出力せよ。
コーディング★★★無料
期待される結果

出力3行が完全一致

ヒント

子の constructor で super(name, 種別, 割引率) を呼び、種別と率だけを変える。計算と表示は親に1回書けばよい。

模範解答
class Member {
  constructor(name, kind = "一般", rate = 0) {
    this.name = name;
    this.kind = kind;
    this.rate = rate;
  }

  finalPrice(price) {
    return Math.round(price * (1 - this.rate));
  }

  toString() {
    return `${this.name}(${this.kind})`;
  }
}

class Premium extends Member {
  constructor(name) {
    super(name, "プレミアム", 0.1);
  }
}

class VIP extends Member {
  constructor(name) {
    super(name, "VIP", 0.2);
  }
}

const members = [new Member("田中"), new Premium("鈴木"), new VIP("佐藤")];

for (const m of members) {
  console.log(`${m}:${m.finalPrice(10000).toLocaleString()}円`);
}
解説

計算式は親に1回だけ書けばよい。会員種別が10種類に増えても、子クラスは constructor 1つずつで済む。JS04で if/elif を並べた割引処理と見比べてほしい。

JS14-S05 【インスタンスは独立している】カウンタを持つ Counter クラスに increment と reset を定義せよ。a と b の2つを作り、a を3回・b を1回増やして「a: 3 / b: 1」と出力し、a だけリセットして「リセット後 a: 0 / b: 1」と出力すること。 コーディング★★★無料
期待される結果

出力2行が完全一致

ヒント

constructor で this.count = 0 と初期化する。a.reset() を呼んでも b は変わらない。

模範解答
class Counter {
  constructor() {
    this.count = 0;
  }

  increment() {
    this.count++;
  }

  reset() {
    this.count = 0;
  }
}

const a = new Counter();
const b = new Counter();

a.increment();
a.increment();
a.increment();
b.increment();

console.log(`a: ${a.count} / b: ${b.count}`);

a.reset();
console.log(`リセット後 a: ${a.count} / b: ${b.count}`);
解説

b が影響を受けないことがこの問題の答え。同じクラスから作っても実物は別物。JS13のモジュールは全体で1つしか存在しないので、ここが両者の使い分けの分かれ目になる。

5仕上げ課題

JS14-FINAL 【ECサイトの注文システム】クラス・継承・getter・自作エラーを使って注文処理を実装せよ。

OutOfStockErrorError を継承(name を "OutOfStockError" に)

Product クラス:name / price / stock を持つ
 ・getter label … "商品" を返す ・getter needsShipping … false を返す
 ・take(qty) … 在庫不足なら throw new OutOfStockError(`「${name}」の在庫が足りません(残り${stock}個)`)、足りていれば在庫を減らす

DigitalProduct:label は "電子"(送料不要のまま)
PhysicalProduct:label は "物理"、needsShipping は true

Order クラス:store と lines(空配列)を持つ
 ・add(product, qty) … take してから lines に { product, qty } を追加
 ・getter count … 合計点数 ・getter subtotal … 商品小計
 ・getter shipping … 送料が必要な商品が1つでもあれば800、なければ0
 ・getter total … 小計+送料

処理:ノートPC(128000, 在庫5)×1、マウス(3200, 在庫2)×2、Python入門eBook(2400, 在庫999)×1 を注文して明細と集計を表示 → 最後にマウスをもう1個追加しようとして失敗する。

出力仕様(11行):
=== ゼロカラストア 注文 ===
[物理] ノートPC:128,000円 × 1
[物理] マウス:3,200円 × 2
[電子] Python入門eBook:2,400円 × 1
---
商品点数:4点
商品小計:136,800円
送料:800円
合計:137,600円
---
エラー:「マウス」の在庫が足りません(残り0個)
期待される結果

出力11行が完全一致

ヒント

送料は lines.some(l => l.product.needsShipping) で判定できる。小計と点数は reduce。最後の追加は try / catch (e) で受け止め、e.message を表示する。

模範解答
class OutOfStockError extends Error {
  constructor(message) {
    super(message);
    this.name = "OutOfStockError";
  }
}

class Product {
  constructor(name, price, stock) {
    this.name = name;
    this.price = price;
    this.stock = stock;
  }

  get label() {
    return "商品";
  }

  get needsShipping() {
    return false;
  }

  take(qty) {
    if (qty > this.stock) {
      throw new OutOfStockError(`「${this.name}」の在庫が足りません(残り${this.stock}個)`);
    }
    this.stock -= qty;
  }
}

class DigitalProduct extends Product {
  get label() {
    return "電子";
  }
}

class PhysicalProduct extends Product {
  get label() {
    return "物理";
  }

  get needsShipping() {
    return true;
  }
}

class Order {
  constructor(store) {
    this.store = store;
    this.lines = [];
  }

  add(product, qty) {
    product.take(qty);
    this.lines.push({ product, qty });
  }

  get count() {
    return this.lines.reduce((sum, l) => sum + l.qty, 0);
  }

  get subtotal() {
    return this.lines.reduce((sum, l) => sum + l.product.price * l.qty, 0);
  }

  get shipping() {
    return this.lines.some((l) => l.product.needsShipping) ? 800 : 0;
  }

  get total() {
    return this.subtotal + this.shipping;
  }
}

const notepc = new PhysicalProduct("ノートPC", 128000, 5);
const mouse = new PhysicalProduct("マウス", 3200, 2);
const ebook = new DigitalProduct("Python入門eBook", 2400, 999);

const order = new Order("ゼロカラストア");
order.add(notepc, 1);
order.add(mouse, 2);
order.add(ebook, 1);

console.log(`=== ${order.store} 注文 ===`);

for (const { product, qty } of order.lines) {
  console.log(`[${product.label}] ${product.name}:${product.price.toLocaleString()}円 × ${qty}`);
}

console.log("---");
console.log(`商品点数:${order.count}点`);
console.log(`商品小計:${order.subtotal.toLocaleString()}円`);
console.log(`送料:${order.shipping.toLocaleString()}円`);
console.log(`合計:${order.total.toLocaleString()}円`);

console.log("---");

try {
  order.add(mouse, 1);
} catch (e) {
  console.log(`エラー:${e.message}`);
}
解説

電子書籍と物理商品という性質の違うものを、Order は区別せず同じように扱えているのが最大の到達点。Order のコードには「電子なら」「物理なら」という分岐が一切ない。送料の要否は商品自身が答えるので、将来「定期購読商品」が増えても Order は無修正で動く。これをポリモーフィズム(多態性)と呼ぶ。

もうひとつの要点は Order がクラスのインスタンスを配列で持っていること。実務のシステムはほぼこの入れ子で、ユーザーが注文を持ち、注文が商品を持つ、という構造で組み立てられる。

JS13のモジュールとの使い分けも整理しておきたい。モジュールは「1つしか存在しないもの」(設定・共通処理)、クラスは「同じ設計の実物がいくつも要るもの」(商品・注文・ユーザー)。どちらもコードを分割する道具だが、目的が違う。

次章では非同期処理を学ぶ。ここまでのコードは書いた順に実行されてきたが、通信のように「待つ」処理が入ると、その前提が崩れる。