JavaScript:クラスと継承
1学習の目的
- class でデータ(プロパティ)と処理(メソッド)を1つにまとめ、同じ設計から独立した実物をいくつも作れるようになる。
- 継承で共通部分を親にまとめ、getter や private フィールドで安全な設計ができるようになる。商品・ユーザー・注文といった「モノ」を素直にコードで表現できるようになる。
2基礎解説
| やりたいこと | 書き方 | 備考 |
|---|---|---|
| 設計図を作る | class Item { } | 名前は大文字始まり |
| 初期化する | constructor(name) { } | new のとき自動で呼ばれる |
| 計算値を持つ | get subtotal() { } | item.subtotal(カッコ不要) |
| 継承する | class Book extends Item { } | super() で親を呼ぶ |
- クラスは「設計図」、インスタンスは「実物」。new で作った実物はそれぞれ独立していて、片方を変えても他方に影響しない。
- this はその実物自身を指す。this.name でそのインスタンスのデータを読み書きする。
- get を付けるとプロパティのように呼べる(item.subtotal、カッコなし)。計算して求まる値に使うと自然に書ける。
- 子クラスで constructor を書いたら必ず先頭で super(…) を呼ぶ。呼ばずに this を使うと ReferenceError になる。
- # を付けたフィールドはクラスの外から一切触れない(プライベート)。書き換えられたくない状態を守れる。
現場使用例:商品・ユーザー・注文などデータのまとまり、残高のような状態を持つもの、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問)
出力 == 田中
constructor(name) の中で this.name = name とする。取り出しは p.name。
class Person {
constructor(name) {
this.name = name;
}
}
const p = new Person("田中");
console.log(p.name);
クラスの最小形。constructor で受け取った値を this に入れておくと、あとからプロパティとして取り出せる。
A. init B. constructor C. create D. new 選択★☆☆無料
解答 == B
「構築する」という意味の英単語。
B
省略もできて、その場合は何もしない constructor があるものとして扱われる。初期値を設定したいときに書く。
出力 == 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) と半径を毎回渡す必要がある。
class Dog {
constructor(name) {
____.name = name;
}
}
console.log(new Dog("ポチ").name);
出力 == ポチ
「これ自身」を意味する4文字のキーワード。
this
this はそのとき操作している実物を指す。100個インスタンスを作れば、それぞれの this が別のオブジェクトを指す。
A. item.subtotal() B. item.subtotal C. item.get.subtotal D. get(item.subtotal) 選択★☆☆無料
解答 == B
getter はプロパティのように振る舞う。
B
カッコを付けないのが getter の特徴。付けると関数そのものを呼ぼうとしてエラーになる。「計算して求まる値」をプロパティらしく見せられる。
出力 == 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 して受け取り直す必要がある。
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() を呼ぶと機械的に覚える。
A. 定数になる B. クラスの外から触れなくなる C. 静的メンバになる D. 自動で初期化される 選択★★☆無料
解答 == B
「プライベート」を表す記号。
B
外から obj.#count と書くと構文エラーになる。触ってほしくない状態を確実に守れるので、残高やトークンなどに使う。
出力 == 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 も自動で変わるので、値がズレる心配がない。
出力 == ワン
class Dog extends Animal と書き、中で同名の speak を定義すれば上書きされる。
class Animal {
speak() {
return "...";
}
}
class Dog extends Animal {
speak() {
return "ワン";
}
}
console.log(new Dog().speak());
オーバーライドの最小形。親と同じ名前で定義するだけで子が優先される。親の処理も使いたければ super.speak() で呼べる。
4実践シナリオ(5問)
出力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 だけを直せばよい。
出力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 に依存しているが、点数を変えれば両方とも自動で追随するのでズレようがない。
残高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 しかないので効かない。不正な操作をクラス自身が拒否する設計になっている。
田中(一般)・鈴木(プレミアム)・佐藤(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 を並べた割引処理と見比べてほしい。
出力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仕上げ課題
OutOfStockError:Error を継承(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つしか存在しないもの」(設定・共通処理)、クラスは「同じ設計の実物がいくつも要るもの」(商品・注文・ユーザー)。どちらもコードを分割する道具だが、目的が違う。
次章では非同期処理を学ぶ。ここまでのコードは書いた順に実行されてきたが、通信のように「待つ」処理が入ると、その前提が崩れる。