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

継承 — 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(親クラスは子クラスのメソッドを持たない)
Book から EBook への継承ツリー
class Bookconstructor・describebook が呼べるのはdescribe だけclass EBookextends BookEBook に書いたのはdownload だけnew EBook(...)のインスタンスdescribe もdownload も呼べる
EBook に書いたのは download だけでも、describe も呼べます。逆に book は download を呼べません

ebook.describe() の中の thisebook なので、Book に書いた this.titlenew EBook に渡した書名を読みます。その書名を ebook に入れたのも、引数を受け取った Bookconstructor です。

管理者がパスワードを初期化したときの記録を表示します。User と targets は宣言済みです。

① User を引き継ぐ AdminUser を定義してください。

② 自分の表示と相手の名前を並べた記録を返す resetPassword を追加してください。

③ 管理者を作り、targets の全員ぶんの記録を表示してください。

④ 管理者が User のインスタンスでもあるかを表示してください。

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

JavaScript / TypeScript エディタ

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

子クラスに値を足す — super() と constructor

電子書籍には、書名と価格に加えてファイルの容量 fileSize も持たせたいとします。容量を受け取るには EBook に自分の constructor を書く必要がありますが、書いた時点で Bookconstructor は自動では呼ばれず、そのまま new すると ReferenceError で止まります。

super()(子クラスの constructor の中で、親クラスの constructor を呼び出す書き方)は、super(title, price); のように親に渡す引数を並べます。

子クラスの thissuper() の中で親の 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
this より先に super() を呼ぶか
this.fileSizeを先に書くsuper() をまだ呼んでいないthis を使う行で止まるnew の時点でReferenceErrorsuper(title,price) を先に書くBook が title とprice を入れるthis.fileSizeに 12 を入れる3 つの値を持つ ebook
上の段は super() より前の this で止まり、インスタンスはできません。this を使えるのは super() が終わった後です

キーが 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 を通して読みます。

管理画面に、管理者ごとの権限を並べます。User と adminInput は宣言済みです。

① User を引き継ぎ、名前・メール・権限の配列を受け取る AdminUser を定義してください。

② 名前とメールは、User の constructor で入れてください。

③ adminInput から管理者を作り、名前と権限の内訳を表示してください。

④ キーを並んだ順に表示し、User に書いた status の位置を確かめてください。

JavaScript / TypeScript エディタ

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

子クラスで送料の計算を変える — オーバーライド

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.shippingFee()ebook.shippingFee()ebook.describe()Book から探しBook で見つかるEBook から探しEBook で見つかるEBook に無くBook で見つかる2640 は 3000未満なので 500Book まで探さずに 0はじめての統計:2200 円
名前を探し始めるのは、インスタンスを作った class です。EBook で見つかると、Book の同じ名前のメソッドは動きません

EBookshippingFeeBook の計算を使わないため、Book の送料の条件を変えても、電子書籍は 0 円のままです。親の処理を呼んだうえで違いだけを書き足す方法は、次の章で扱います。

社内掲示板で、投稿ごとに編集ボタンを出すかを決めます。User と posts は宣言済みです。

① User を引き継ぐ AdminUser を定義し、どの投稿でも編集できる判定に書き直してください。

② 一般ユーザー「鈴木」と管理者「佐藤」を作ってください。

③ 投稿ごとに、2 人が編集できるかを「投稿 ◯: 鈴木 ◯ / 佐藤 ◯」の形で表示してください。

JavaScript / TypeScript エディタ

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

親の処理に書き足す — super.メソッド名()

電子書籍の表示だけ、末尾に「(電子版 12 MB)」を付けたいとします。EBookdescribe をオーバーライドして「書名: 価格 円」の組み立てまで書き写すと、Book の表示の形を変えたときに、電子書籍の表示だけが古い形のまま残ります。

super.メソッド名()(子クラスのメソッドの中から、親クラスに書いたメソッドを呼び出す書き方)を使うと、super.describe()Bookdescribe の戻り値を返します。子クラスは、その文字列に電子版の情報を書き足すだけです。

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)
ebook.describe() の中で動く順番
ebook.describe()の実行を始めるthis は ebook(容量 12 も持つ)super.describe() でBook の describe を実行this は ebook のままはじめての統計: 2200 円戻り値の後ろに(電子版 12 MB)を足すはじめての統計: 2200 円(電子版 12 MB)を返す
Book の describe は EBook の describe の途中で動いて戻ります。書名と価格の組み立ては Book にだけあります

super.describe()Book の戻り値を受け取る式なので、電子版の情報を前に付けたいときも、テンプレートリテラルの中で書く位置を変えるだけです。下の表は、この記事で使った継承の書き方と、それぞれで動くものを並べたものです。

書き方書く場所動くもの
class EBook extends Bookclass の宣言Book の constructor とメソッドを引き継ぐ
super(title, price)子クラスの constructor(this より前)Book の constructor
親と同じ名前のメソッド子クラスの本体EBook のインスタンスでは EBook の定義
super.describe()子クラスのメソッドの中Book の describe

super を省くと自分を呼び続ける

子クラスの describesuper.describe()this.describe() と書くと、自分を呼び続けて RangeError: Maximum call stack size exceeded で止まります。同じ名前で書き直したメソッドから親の処理を呼ぶには、super. を付けます。

社内ツールで、管理者のパスワードだけ条件を厳しくします。User・AdminUser・admin・member と passwords は宣言済みです。

① AdminUser に、User の確認を通った後で 12 文字以上かも確かめる checkPassword を追加してください。

② 管理者で、passwords を順に確かめた結果を表示してください。

③ 一般ユーザーで、2 つ目のパスワードの結果を表示してください。

JavaScript / TypeScript エディタ

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

理解度チェック

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

Q1super() より前で this.fileSize に代入した EBooknew すると?

Q2BookEBook の両方に shippingFee があるとき、ebook.shippingFee() で動くのは?

Q3EBookdescribe の中で、Bookdescribe の戻り値を受け取る書き方は?