5.6 継承とオーバーライド

(1/1)
バトンを渡す人のイラスト(運動会)
JavaScriptは、あからじめ用意されたオブジェクトなどを、ユーザー定義クラスに継承することができる。継承のメリットは、ユーザーが作るプログラム量を減らし、保守性を向上させることだ。
設計者には、変数を扱うより高度な抽象化能力が求められるが、基本的には、何を(オブジェクト=モノ、ヒト、カネ、情報、業務など)クラスにするかを明確にし、オブジェクトの性質(プロパティ、メソッド)を整理すれば、継承関係を作る必要性も見えてくる。

目次

サンプル・プログラム

圧縮ファイルの内容
countKanji2.htmlテキストの読みやすさを調べる(String2クラス)
monthlyCalendar3.html1ヶ月カレンダー
Date2.jsDate2クラス

String2クラス

5.5 クラス」で作った文字カウント・クラス countKanji だが、String型の lengthプロパティに似ている。
JavaScriptは、あらかじめ用意されたオブジェクトにプロペティやメソッドを追加する形で、あらたなクラスを定義する継承という機能がある。そして、プロミティブ型である String型を継承することもできる。
ここでは、String型を継承するString2クラスを作り、そこに countKanji クラスの内容を実装してみる。
テキストの読みやすさを調べる

  53: // String2クラス ============================================================
  54: class String2 extends String {
  55: 
  56: /**
  57:  * コンストラクタ(オーバーライド)
  58:  * @param   String text テキスト
  59:  * @return  なし
  60: */
  61: constructor(text) {
  62:     super(text);
  63:     //プロパティ
  64:     this.text = text;       //テキスト
  65:     this.moji   = 0;        //文字数
  66:     this.kanji  = 0;        //漢字の数
  67:     this.kuten  = 0;        //句点の数
  68:     this.toten  = 0;        //読点の数
  69: 
  70:     //文字数をカウントする
  71:     for (let i = 0i < this.text.lengthi++) {
  72:         let ch = this.text.substr(i, 1);
  73:         if (ch.match(/[一-龠]+/g)) {
  74:             this.moji++;
  75:             this.kanji++;
  76:         } else if (ch.match(/[。。..]+/g)) {
  77:             this.kuten++;
  78:         } else if (ch.match(/[、、,,]+/g)) {
  79:             this.toten++;
  80:         } else if (! ch.match(/[\n\r]+/g)) {
  81:             this.moji++;
  82:         }
  83:     }
  84: }
  85: }
  86: // End of Class ============================================================

String型を継承するには、class宣言において extends String と書く。extends キーワードは継承することを意味する。
もともと countKanji クラスは constructor しかなかった。そこで、String型のconstructorに機能を追加することになるわけだが、この仕組みを[オーバーライド]と呼ぶ。JavaScriptでは、constructor の冒頭で superキーワードを使って継承元オブジェクトの引数を呼んでおく。
これ以降は、countKanji クラスと同じである。

 115: /**
 116:  * 文字数等をカウントして表示する.
 117:  * @param   なし
 118:  * @return  なし
 119: */
 120: function countCharacters() {
 121:     //文字数カウント
 122:     let str = new String2(document.getElementById('sour').value)
 123: 
 124:     //結果を表示
 125:     document.getElementById('length').innerHTML = str.length.toLocaleString() + '文字';
 126:     document.getElementById('moji').innerHTML = str.moji.toLocaleString() + '文字';
 127:     document.getElementById('kanji').innerHTML = (str.kanji / str.moji * 100).toLocaleString() + '%';
 128:     document.getElementById('sentence_length').innerHTML = (str.moji / str.kuten).toLocaleString() + '文字';
 129:     document.getElementById('phrase_length').innerHTML = (str.moji / (str.kuten + str.toten)).toLocaleString() + '文字';
 130: }

ユーザー定義関数 countCharacters では、String.lengthプロパティを表示するように追加した。このように、String2オブジェクトではStringオブジェクトのプロパティも継承されていることが分かる。

Date2クラス

次に、Dateオブジェクトを継承し、「5.5 クラス」で作ったクラス pahooCalendar を組み込んだクラス Date2 を作ってみよう。
1ヵ月カレンダー(Date2クラス)

  11: // カレンダー・クラス ======================================================
  12: class Date2 extends Date {
  13: 
  14: /**
  15:  * うるう年かどうかを判定する
  16:  * @param   なし
  17:  * @return  Boolean true:うるう年/false:平年
  18: */
  19: isleap() {
  20:     let year = this.getFullYear();      //西暦年
  21:     let ret  = false;                   //戻り値
  22: 
  23:     if (year % 400 === 0) {
  24:         ret = true;
  25:     } else if (year % 100 === 0) {
  26:         ret = false;
  27:     } else if (year % 4 === 0) {
  28:         ret = true;
  29:     } else {
  30:         ret = false;
  31:     }
  32: 
  33:     return ret;
  34: }
  35: 
  36: /**
  37:  * 指定した月の日数を返す
  38:  * @param   なし
  39:  * @return Number 月の日数/(-1):引数エラー
  40: */
  41: getDaysInMonth() {
  42:     let year  = this.getFullYear();
  43:     let month = this.getMonth() + 1;
  44: 
  45:     //月の日数
  46:     let days = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
  47: 
  48:     //うるう年判定
  49:     days[2] = this.isleap(year? 29 : 28;
  50: 
  51:     return days[month];
  52: }
  53: 
  54: }
  55: // End of Class ======================================================

ユーザー定義クラス Date2Dateオブジェクトを継承する。
get_week_number の代わりに、継承元にあらかじめ備わっている getDayメソッドを用いることにする。

 117: /**
 118:  * 1ヵ月カレンダーを表示する.
 119:  * @param   なし
 120:  * @return  なし
 121: */
 122: function printMonthlyCalendar() {
 123:     //表示クリア
 124:     document.getElementById('error').innerHTML = '';
 125:     document.getElementById('calendar').innerHTML = '';
 126: 
 127:     //年月を取得
 128:     let year  = parseInt(document.getElementById('year').value,  10);
 129:     let month = parseInt(document.getElementById('month').value, 10);
 130: 
 131:     //Date2オブジェクト
 132:     dt2 = new Date2(year.toString() + '-' + month.toString() + '-01 00:00:00');
 133:     if (isNaN(dt2)) {
 134:         //エラーを表示
 135:         document.getElementById('error').innerHTML = 'エラー:正しい年月を入力してください.';
 136:         return;
 137:     }
 138: 
 139:     //月の日数,1日の曜日番号を取得する
 140:     let daysInMonth = dt2.getDaysInMonth();
 141:     let startWeekNumber = dt2.getDay();
 142: 
 143:     //バリデーション
 144:     if ((daysInMonth == (-1)) || (startWeekNumber == (-1))) {
 145:         //エラーを表示
 146:         document.getElementById('error').innerHTML = 'エラー:正しい月を入力してください.';
 147: 
 148:     //カレンダーの作成と表示.
 149:     } else {
 150:         document.getElementById('calendar').innerHTML = makeCalendar(daysInMonth, startWeekNumber);
 151:     }
 152: 
 153:     //Date2オブジェクトを解放する.
 154:     dt2 = null;
 155: }

ユーザー定義関数 printMonthlyCalendar から Date2 クラスを呼び出している。getDayメソッドが有効に機能していることが分かる。
継承のメリットは、ユーザーが作るプログラム量を減らし、保守性を向上させることだ。継承元のバグも引き継いでしまうというデメリットはあるが、1箇所修正すれば他の部分も修正できる。

コラム:スーパークラス、サブクラス

クラスの継承
本編では、あかじめ用意されているオブジェクトの継承を例にしたが、ユーザーが定義したクラスも継承できる。継承元のクラスをスーパークラス、継承したクラスをサブクラス(派生クラス)と呼ぶ。

上図のように、スーパークラス(左)は「自動車」のようなオブジェクトの基本属性を用意しておき、そこから派生する「トラック」「バス」はサブクラス(右)として、各々に固有のプロパティやメソッドを追加しておくといい。

クラスや継承は、JavaScriptに限らず、今どきのオブジェクト指向プログラミングで必ず覚えなければならない概念である。設計者には、変数を扱うより高度な抽象化能力が求められるが、基本的には、何を(オブジェクト=モノ、ヒト、カネ、情報、業務など)クラスにするかを明確にし、オブジェクトの性質(プロパティ、メソッド)を整理すれば、継承関係を作る必要性も見えてくるだろう。
(この項おわり)
header