5.5 クラス

(1/1)
おもちゃのカプセル
JavaScriptは、あからじめ用意されたオブジェクト以外にも、ユーザーがクラスと呼ぶオブジェクトのテンプレートを定義することができる。
クラスを別ファイルにすることで、再利用や保守が容易になる。このように、処理を再利用や保守が容易になるように分離することをカプセル化と呼ぶ。

目次

サンプル・プログラム

文字カウント・クラス

5.4 正規表現」で学んだ方法を使って、文字数や文の長さなどをカウントするプログラムを作ってみることにする。
テキストの読みやすさを調べる

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

ここでは、文字数などをカウントするユーザー定義クラスが countKanji である。クラスは、class宣言を使って定義する。

constructorは、オブジェクトが生成されたときに一度だけ実行される特殊なメソッドで、そのオブジェクトの初期化などに用いる。メソッドの記述方法は、関数と同じである。引数を渡すことができる。constructor の中で this.名前 で示された変数はプロパティになる。
ここでは、constructor の中で、プロパティとして文字数、漢字の数、句点の数、読点の数を、それぞれ代入している。漢字、句点、読点文字の識別に正規表現を用いている。

countKanji クラスは、constructor 以外のメソッドは持っていない。

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

ユーザー定義関数 countCharacters から countKanji クラスを呼び出している。new 演算子でオブジェクト生成する。このとき、countKanji クラスの constructor が実行されるので、あとはプロパティを参照して文の長さなどを計算する。
プロパティの参照は、JavaScriptに備わっているオブジェクトと同様、オブジェクト名.プロパティ名という名前で呼び出す。

1ヵ月カレンダーを表示する

次に、「4.3 配列」で紹介した1ヵ月カレンダー "monthlyCalendar.html" をクラス化してみよう。
1ヵ月カレンダー(クラス編)

  12: // カレンダー・クラス ======================================================
  13: class pahooCalendar {
  14: 
  15: /**
  16:  * コンストラクタ
  17:  * @param   Number year, month, day 年月日
  18:  * @return  なし
  19: */
  20: constructor(year, month, day) {
  21:     //プロパティ
  22:     this.year  = year;      //西暦年
  23:     this.month = month;     //月
  24:     this.day   = day;       //日
  25: }
  26: 
  27: /**
  28:  * うるう年かどうかを判定する
  29:  * @param   なし
  30:  * @return  Boolean true:うるう年/false:平年
  31: */
  32: isleap() {
  33:     let ret;    //戻り値
  34: 
  35:     if (this.year % 400 === 0) {
  36:         ret = true;
  37:     } else if (this.year % 100 === 0) {
  38:         ret = false;
  39:     } else if (this.year % 4 === 0) {
  40:         ret = true;
  41:     } else {
  42:         ret = false;
  43:     }
  44: 
  45:     return ret;
  46: }
  47: 
  48: /**
  49:  * 指定した月の日数を返す
  50:  * @param   なし
  51:  * @return Number 月の日数/(-1):引数エラー
  52: */
  53: getDaysInMonth() {
  54:     //引数のバリデーション
  55:     if (! Number.isInteger(this.year|| ! Number.isInteger(this.month||
  56:         this.month < 1 || this.month > 12) {
  57:         return (-1);
  58:     }
  59: 
  60:     //月の日数
  61:     let days = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
  62: 
  63:     //うるう年判定
  64:     days[2] = this.isleap(this.year? 29 : 28;
  65: 
  66:     return days[this.month];
  67: }
  68: 
  69: /**
  70:  * 曜日番号を求める(ツェラーの公式)
  71:  * @param   なし
  72:  * @return  Number 曜日番号(0:日曜日, 1:月曜日...6:土曜日)/(-1):引数エラー
  73: */
  74: getWeekNumber() {
  75:     let year  = this.year;
  76:     let month = this.month;
  77:     let day   = this.day;
  78: 
  79:     //引数のバリデーション
  80:     if (! Number.isInteger(year|| ! Number.isInteger(month||
  81:         ! Number.isInteger(day||
  82:         month < 1 || month > 12) {
  83:         return (-1);
  84:     }
  85:     if (day < 1 || day > this.getDaysInMonth(year, month)) {
  86:         return (-1);
  87:     }
  88: 
  89:     //ツェラーの公式の変形
  90:     if (month <2) {
  91:         month +12;
  92:         year--;
  93:     }
  94:     let c = Math.floor(year / 100);
  95:     let y = year % 100;
  96:     let h = (5 * c + y + Math.floor(y / 4+ Math.floor(c / 4+ Math.floor(26 * (month + 1) / 10+ day - 1% 7;
  97: 
  98:     return h;
  99: }
 100: 
 101: }
 102: // End of Class ======================================================

ユーザー定義クラスが pahooCalendar でに、「4.3 配列」で紹介した isleap関数、getDaysInMonth関数、get_week_number関数をメソッドとして実装した。メソッドの場合、関数と違って function を使わず、いきなりメソッド名を記述する。
また、constructor に、あらかじめ年、月、日を渡すことで、メソッドを呼ぶ都度、いちいち年や月を引数と渡す手間を省いた。

 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:     //カレンダー・オブジェクトを生成する.
 132:     let pcd = new pahooCalendar(year, month, 1);
 133: 
 134:     //月の日数,1日の曜日番号を取得する
 135:     let daysInMonth = pcd.getDaysInMonth();
 136:     let startWeekNumber = pcd.getWeekNumber(year, month, 1);
 137: 
 138:     //バリデーション
 139:     if ((daysInMonth == (-1)) || (startWeekNumber == (-1))) {
 140:         //エラーを表示
 141:         document.getElementById('error').innerHTML = 'エラー:正しい月を入力してください.';
 142: 
 143:     //カレンダーの作成と表示.
 144:     } else {
 145:         document.getElementById('calendar').innerHTML = makeCalendar(daysInMonth, startWeekNumber);
 146:     }
 147: 
 148:     //カレンダー・オブジェクトを解放する.
 149:     pcd = null;
 150: }

ユーザー定義関数 printMonthlyCalendar から pahooCalendar クラスを呼び出している。new演算子でオブジェクト生成したり、メソッドを利用するなどしている。
ユーザーがクラスを定義する最大のメリットは、それを再利用できると言うことだ。
ここでは、pahooCalendar クラスを別ファイル "pahooCalendar1.js" にした。
下記のようにHTML中で "pahooCalendar1.js" と呼び出すことで、他のプログラムでも容易に再利用できる。

1ヵ月カレンダーをTABLEタグとして返す

月の日数と1日の曜日番号を指定し、1ヵ月カレンダーをTABLEタグとして返す処理をユーザー関数 makeCalendar として分離した。

  57: /**
  58:  * 月の日数と1日の曜日番号を指定し,1ヵ月カレンダーをTABLEタグとして返す.
  59:  * @param   Number daysInMonth     月の日数
  60:  * @param   Number startWeekNumber 1日の曜日番号
  61:  * @return  String TABLEタグ
  62: */
  63: function makeCalendar(daysInMonth, startWeekNumber) {
  64:     //カレンダーの行列サイズ
  65:     const MAX_ROW = 6;
  66:     const MAX_COL = 7;
  67:     //曜日の種類
  68:     const SUNDAY   = 'sunday';
  69:     const SATURDAY = 'saturday';
  70:     const WEEKDAY  = 'weekday';
  71:     //曜日表
  72:     const WEEK_LIST = [SUNDAY, WEEKDAY, WEEKDAY, WEEKDAY, WEEKDAY, WEEKDAY, SATURDAY];
  73: 
  74:     //カレンダー配列を準備する.
  75:     let calendarArray = newArray2d(MAX_ROW, MAX_COL, 0);
  76: 
  77:     //カレンダーの1行目
  78:     let html =`
  79: <tr>
  80: <th class="sunday">日</th>
  81: <th class="weekday">月</th>
  82: <th class="weekday">火</th>
  83: <th class="weekday">水</th>
  84: <th class="weekday">木</th>
  85: <th class="weekday">金</th>
  86: <th class="saturday">土</th>
  87: </tr>
  88: `;
  89: 
  90:     //カレンダー配列に日付を格納する.
  91:     for (let day = 1, row = 0, col = startWeekNumberday <daysInMonthday++, col++) {
  92:         //週の終わりに達したら,次の週へ
  93:         if (col >MAX_COL) {
  94:             col = 0;
  95:             row++;
  96:         }
  97:         //日付を代入
  98:         calendarArray[row][col] = day;
  99:     }
 100: 
 101:     //カレンダー配列をTABLEタグに展開する.
 102:     calendarArray.forEach (function (rows, row) {
 103:         if ((row > 0&& (rows[0] == 0))    return;     //空行ならスキップ
 104:         //1週間分を展開する.
 105:         html +="<tr>";
 106:         rows.forEach (function(val, weekNumber) {
 107:             html +'<td class="' + WEEK_LIST[weekNumber+'">';
 108:             html += (val > 0? val : '&nbsp;';
 109:             html +'</td>';
 110:         });
 111:         html +"</tr>";
 112:     });
 113: 
 114:     return html;
 115: }

カレンダー配列に日付を格納する処理は、for文に変更した。for文の中でインクリメントする変数 daycol は、ご覧のように、1行にまとめて記述でき、読みやすいプログラムになるからだ。

2次元配列を初期化する

カレンダー配列となる2次元配列を用意して初期化する処理を、ユーザー関数 newArray2d として分離した。

  37: /**
  38:  * 2次元配列を用意し,初期化する.
  39:  * @param   Number rows 行数
  40:  * @param   Number cols 列数
  41:  * @param   Number val  初期値
  42:  * @return  Array 2次元配列
  43: */
  44: function newArray2d(rows, cols, val=0) {
  45:     /** これと同じ作用をする.
  46:     *   let arr = new Array(rows);
  47:     *   for (let i = 0; i < rows; i++) {
  48:     *       arr[i] = new Array(cols);
  49:     *       for (let j = 0; j < cols; j++) {
  50:     *           arr[i][j] = val;
  51:     *       }
  52:     *   }
  53:     */
  54:     return Array.from(new Array(rows), _ => new Array(cols).fill(val));
  55: }

制御を使わずに1行で実現した。こういう処理の場合、後ろから見ていくと分かりやすい。
fillメソッドは、配列の全ての要素に指定した値を代入する。配列は Arrayオブジェクトとして指定した要素の数 cols を生成する。
fromメソッドは、配列の要素を1つずつコールバック関数に渡し、コールバック関数の戻り値から新しい配列を生成する。この作用を利用し、2次元配列を初期化している。

IE対応

次に、pahooCalendarクラス をIE対応にする方法を紹介する。
2.1 変数と定数」で、JavaScript ES6(ES2015) に則ると宣言した。ChromeやSafariなどのモダンブラウザであれば、それでいいのだが、世の中にはまだ IE11 ユーザーが多い。そして、IE11 では JavaScript ES6(ES2015) に準拠していない。
IEでは class宣言 をはじめ、いくつかの命令が備わっておらず、これらを代替しつつ、モダンブラウザでも動作する手法を紹介しておくことにする。

  12: //IE用isInteger
  13: Number.isInteger = Number.isInteger ||
  14:     function (n) {
  15:         return (typeof n === 'number'&& isFinite(n&& (Math.floor(n) === n);
  16:     };

まず、細かいところから――。
IEは Number.isInteger をサポートしていない。そこで、代替のメソッドを用意した。

JavaScriptでは、すでにあるメソッドを上書き(オーバーライド)できる。
これを利用し、Number.isIntegerが存在すれば、そのままオーバーライドする。無ければ(||演算子)、function以下の処理をメソッドとしてオーバーライドする。
まず、typeof演算子 を使って引数がNumber型であること。なおかつ、isFinite 関数を使ってInfinity、NaN、undefinedのいずれでもないこと。なおかつ、floor メソッドを使って、n以下の最大の整数がn自身であること。これらが揃えば整数であることは自明なので、trueを返す。
次に、fillメソッドfromメソッド がない。そこで、先ほどの newArray2d 関数でコメント内に記載したコードに置き換えてやる必要がある。

  37: /**
  38:  * 2次元配列を用意し,初期化する.
  39:  * @param   Number rows 行数
  40:  * @param   Number cols 列数
  41:  * @param   Number val  初期値
  42:  * @return  Array 2次元配列
  43: */
  44: function newArray2d(rows, cols, val) {
  45:     let arr = new Array(rows);
  46:     for (let i = 0i < rowsi++) {
  47:         arr[i] = new Array(cols);
  48:         for (let j = 0j < colsj++) {
  49:             arr[i][j] = val;
  50:         }
  51:     }
  52:     return arr;
  53: }

さらに、「4.2 ユーザー定義関数」で紹介したように、IEではテンプレートリテラルが利用できない。テンプレートリテラルを使わないようにした。

最後に――これが一番大きな変更点だが――IEは class宣言 が使えない。そこで、代替手段として、グローバル空間で無名関数に名前(オブジェクト名)を与える処理をクラスに見立てる。この部分がコンストラクタになる。

  18: /**
  19:  * コンストラクタ
  20:  * @param   Number year, month, day 年月日
  21:  * @return  なし
  22: */
  23: pahooCalendar = function (year, month, day) {
  24:     this.year  = year;
  25:     this.month = month;
  26:     this.day   = day;
  27: }

コンストラクタの中で、this を使って示しているプロパティの実体は、prototype プロパティによってクラス(オブジェクト名)に追加しておく。

  29: //プロパティ
  30: pahooCalendar.prototype.year  = 0;      //西暦年
  31: pahooCalendar.prototype.month = 0;      //月
  32: pahooCalendar.prototype.day   = 0;      //日
  33: 

  34: /**
  35:  * うるう年かどうかを判定する
  36:  * @param   なし
  37:  * @return  Boolean true:うるう年/false:平年
  38: */
  39: pahooCalendar.prototype.isleap = function () {
  40:     let ret;    //戻り値
  41: 
  42:     if (this.year % 400 === 0) {
  43:         ret = true;
  44:     } else if (this.year % 100 === 0) {
  45:         ret = false;
  46:     } else if (this.year % 4 === 0) {
  47:         ret = true;
  48:     } else {
  49:         ret = false;
  50:     }
  51: 
  52:     return ret;
  53: }

メソッドも、このように prototypeプロパティ を使って無名関数をクラス(オブジェクト名)に追加してゆく。

 119:     //カレンダー・オブジェクトを生成する.
 120:     let pcd = new pahooCalendar(year, month, 1);

クラスの呼び出しは、JavaScript ES6(ES2015) と同じである。

カプセル化

どのようなプロパティや関数を一括りにしてクラスにするかという作業をカプセル化と呼ぶ。適切にカプセル化したクラスは再利用や保守がしやすいものである。

どのようなポリシーでカプセル化するかは、複数のプログラムを作ってみないと決まらない。複数のプログラムで繰り返し使用するような処理は、カプセル化することで開発作業を省力化できる。会社によっては、自社でカプセル化したパッケージを持っている。
まずはユーザー関数として定義しておき、次のプログラムを作るときにメソッドにするかどうか考えればいいだろう。
逆に言うと、そのプログラム内は何回も呼び出すが、他のプログラムで利用しないような処理はユーザー定義関数のままで十分である。

なお、少なくともHTML文とデータの受け渡しをするような処理はカプセル化には相応しくない。HTML文のid名はファイルによって変わるものだから、カプセル化に馴染まない。
今回のように、データ受け渡し処理はユーザー定義関数に任せ、そこからクラスを呼び出すようにするのが無難だ。

コラム:何をカプセル化するか

赤道座標系
何でもかんでもカプセル化すればいいという話ではない。
ソフト開発会社によってはルールを決めているところもあるし、そうでない場合は、プロジェクトやチームでルール決めをしておいた方がいいだろう。

たとえば演算結果が単純な整数ではないもの――球面三角法がこれに当たる。Mathオブジェクトの三角関数は、単位がラジアンであるが、地図座標(緯度・経度)は度分秒に変換した方が扱いが楽である。また、経度は0以上360未満だが、緯度は-90度以上+90度以下という違いもある。
天球座標系の一種の赤道座標系では、赤経が時分秒(0時0分0秒~23時59分59秒)、赤緯が-90度~+90度になる。
Mathオブジェクトを継承し、こうした系に即したメソッドを用意するのが無難である。

日付(年月日)や時刻(時分秒)も独特な系である。Dateオブジェクトを継承し、旧暦計算をメソッドとして加えるといいだろう。
なお、継承については、次回説明する。
(この項おわり)
header