Q1super() より前で this.fileSize に代入した EBook を new すると?
継承 — extends と super
既存の class を引き継いで新しい class を作る継承を扱います。extends の書き方、子クラスの constructor で呼ぶ super()、オーバーライド、親のメソッドを呼ぶ super.メソッド名() を学べます。
書籍の通販サイトで、紙の本の Book とは別に電子書籍の class を作るとします。違うのはダウンロードできることと送料がかからないことだけなのに、今の書き方では書名や価格を扱う部分まで書き直すことになります。
この記事では、既存の class を引き継ぐ extends と、引き継いだ class の処理を呼び出す super を扱います。
同じ処理を書き写さずに引き継ぐ — extends
電子書籍にも、書名と価格を受け取る constructor と、「書名: 価格 円」を返す describe が要るとします。Book の本体を写して電子書籍の class を作ると同じ行が 2 か所に並び、表示の形を変えたときに片方を直し忘れると、画面によって表示がずれます。
継承(既存の class の constructor とメソッドを、別の class でも使えるようにする仕組み)は class EBook extends Book と書き、Book を親クラス、EBook を子クラスと呼びます。
throw とカスタムエラーの記事で書いた extends Error も、この継承です。
class Book {
constructor(title, price) {
this.title = title;
this.price = price;
}
describe() { return `${this.title}: ${this.price} 円`; }
}
// Book を引き継ぎ、電子書籍だけが持つメソッドを足す
class EBook extends Book {
download() { return `${this.title} をダウンロードします`; }
}
const ebook = new EBook("はじめての統計", 2200); // 引数は Book の constructor に渡る
console.log(ebook.describe()); // はじめての統計: 2200 円
console.log(ebook.download()); // はじめての統計 をダウンロードします
const book = new Book("はじめての統計", 2640);
console.log(typeof book.download); // undefined(親クラスは子クラスのメソッドを持たない)
ebook.describe() の中の this は ebook なので、Book に書いた this.title は new EBook に渡した書名を読みます。その書名を ebook に入れたのも、引数を受け取った Book の constructor です。
子クラスに値を足す — super() と constructor
電子書籍には、書名と価格に加えてファイルの容量 fileSize も持たせたいとします。容量を受け取るには EBook に自分の constructor を書く必要がありますが、書いた時点で Book の constructor は自動では呼ばれず、そのまま new すると ReferenceError で止まります。
super()(子クラスの constructor の中で、親クラスの constructor を呼び出す書き方)は、super(title, price); のように親に渡す引数を並べます。
子クラスの this は super() の中で親の constructor が用意するため、this を使う行より前に呼びます。
class Book {
constructor(title, price) {
this.title = title;
this.price = price;
}
}
class EBook extends Book {
constructor(title, price, fileSize) {
// この行より前に this.fileSize = fileSize; を書くと、new の時点で ReferenceError
super(title, price); // Book の constructor が title と price を入れる
this.fileSize = fileSize; // その後で、EBook だけが持つ値を足す
}
}
const ebook = new EBook("はじめての統計", 2200, 12);
console.log(ebook.title); // はじめての統計
console.log(ebook.fileSize); // 12
console.log(Object.keys(ebook).join(", ")); // title, price, fileSize
キーが title, price, fileSize の順なのは、super() の中で Book が先に入れたためです。上の段では ReferenceError: Must call super constructor in derived class before accessing 'this' … と表示され、derived class は子クラスを指します。
親の # フィールドは子クラスで読めない
Book が private フィールド #price を持つ場合、super() で値は入りますが、EBook の本体に this.#price と書くと SyntaxError で止まります。# の名前は宣言した class の中だけで使えるため、子クラスからは親の getter を通して読みます。
子クラスで送料の計算を変える — オーバーライド
Book に送料を返す shippingFee を書くと、電子書籍にも紙の本と同じ送料が付きます。電子書籍だけ 0 円にしようと Book の中で本の種類を if で分けると、子クラスが増えるたびに親クラスの分岐が長くなります。
オーバーライド(親クラスにあるメソッドと同じ名前のメソッドを子クラスに書き、子クラスのインスタンスではそちらを動かすこと)は、EBook の本体に shippingFee を書き直すだけです。メソッドを呼ぶと、インスタンスを作った class から名前を探し、無ければ親クラスを探します。
class Book {
constructor(title, price) {
this.title = title;
this.price = price;
}
describe() { return `${this.title}: ${this.price} 円`; }
shippingFee() { return this.price >= 3000 ? 0 : 500; } // 3000 円以上は送料無料
}
class EBook extends Book {
shippingFee() { return 0; } // 同じ名前で書き直す(届ける荷物が無い)
}
const book = new Book("はじめての統計", 2640);
const ebook = new EBook("はじめての統計", 2200);
console.log(book.shippingFee()); // 500
console.log(ebook.shippingFee()); // 0(EBook の shippingFee が動く)
console.log(ebook.describe()); // はじめての統計: 2200 円(Book の describe が動く)
EBook の shippingFee は Book の計算を使わないため、Book の送料の条件を変えても、電子書籍は 0 円のままです。親の処理を呼んだうえで違いだけを書き足す方法は、次の章で扱います。
親の処理に書き足す — super.メソッド名()
電子書籍の表示だけ、末尾に「(電子版 12 MB)」を付けたいとします。EBook で describe をオーバーライドして「書名: 価格 円」の組み立てまで書き写すと、Book の表示の形を変えたときに、電子書籍の表示だけが古い形のまま残ります。
super.メソッド名()(子クラスのメソッドの中から、親クラスに書いたメソッドを呼び出す書き方)を使うと、super.describe() が Book の describe の戻り値を返します。子クラスは、その文字列に電子版の情報を書き足すだけです。
class Book {
constructor(title, price) {
this.title = title;
this.price = price;
}
describe() { return `${this.title}: ${this.price} 円`; }
}
class EBook extends Book {
constructor(title, price, fileSize) {
super(title, price);
this.fileSize = fileSize;
}
// Book の describe の戻り値に、電子版の情報を書き足す
describe() { return `${super.describe()}(電子版 ${this.fileSize} MB)`; }
}
const ebook = new EBook("はじめての統計", 2200, 12);
console.log(ebook.describe()); // はじめての統計: 2200 円(電子版 12 MB)
super.describe() は Book の戻り値を受け取る式なので、電子版の情報を前に付けたいときも、テンプレートリテラルの中で書く位置を変えるだけです。下の表は、この記事で使った継承の書き方と、それぞれで動くものを並べたものです。
| 書き方 | 書く場所 | 動くもの |
|---|---|---|
| class EBook extends Book | class の宣言 | Book の constructor とメソッドを引き継ぐ |
| super(title, price) | 子クラスの constructor(this より前) | Book の constructor |
| 親と同じ名前のメソッド | 子クラスの本体 | EBook のインスタンスでは EBook の定義 |
| super.describe() | 子クラスのメソッドの中 | Book の describe |
super を省くと自分を呼び続ける
子クラスの describe で super.describe() を this.describe() と書くと、自分を呼び続けて RangeError: Maximum call stack size exceeded で止まります。同じ名前で書き直したメソッドから親の処理を呼ぶには、super. を付けます。
理解度チェック
まずは1問ずつ答えてみましょう。
Q2Book と EBook の両方に shippingFee があるとき、ebook.shippingFee() で動くのは?
Q3EBook の describe の中で、Book の describe の戻り値を受け取る書き方は?