オブジェクト指向的なプログラムの書き方
こう切り出しても、本人はメソッドだとかの専門用語を覚えていない。というかもう覚えても忘れる。^^;
例えば、呼び出された回数を数えるプログラム(カウンター)を書くにはどうする?
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"> <title>TEST</title> <body> <form action="#"> <p> <input type="text" id="data1" value=""> </p> <p> <input type="text" id="data2" value=""> </p> </form> <script type="text/javascript"> var counter = 0; function up() { counter += 1; return counter; } function disp() { var n = up(); document.getElementById('data1').value = n + ''; } setInterval( disp, 1000 );//1秒おきに、カウントを表示する </script>
でも、これを違うタイミングで動作する別々なカウンターが必要になったらどうする?
変数cntを複数作りcnt2,cnt3 見たいにして counterも複数、dispも複数、必要な数だけ用意する?
配列をグローバルにしてやれば、できそうだけど、ここはオブジェクトにして考える
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"> <title>TEST</title> <body> <form action="#"> <p> <input type="text" id="data1" value=""> </p> <p> <input type="text" id="data2" value=""> </p> </form> <script type="text/javascript"> function Counter() { var cnt = 0; this.up = function () { return ++cnt; } } var counter1 = new Counter; var counter2 = new Counter; function disp1() { var n = counter1.up(); document.getElementById('data1').value = n ; } function disp2() { var n = counter2.up(); document.getElementById('data2').value = n + ''; } setInterval( disp1, 1000 ); setInterval( disp2, 500 ); </script>
dispもオブジェクトにしてしまう
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"> <title>TEST</title> <body> <form action="#"> <p> <input type="text" id="data1" value=""> </p> <p> <input type="text" id="data2" value=""> </p> </form> <script type="text/javascript"> function Counter() { var cnt = 0; this.up = function () { return ++cnt; }; } function Disp( cnt, id ) { var e = document.getElementById( id ); var c = cnt; this.disp = function() { e.value = c.up(); }; } var disp1 = new Disp( new Counter, 'data1' ); var disp2 = new Disp( new Counter, 'data2' ); setInterval( disp1.disp, 1000 ); setInterval( disp2.disp, 500 ); </script>
今度は、タイマーもオブジェクトにしてしまう。
タイマーにスタートボタンとストップボタンをつけて、
タイマーをコントロールできるように拡張する
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"> <title>TEST</title> <body> <form action="#"> <p> <input type="text" id="data1" value=""> <input type="button" value="start" onClick="timer1.start()"> <input type="button" value="stop" onClick="timer1.stop()"> </p> <p> <input type="text" id="data2" value=""> <input type="button" value="start" onClick="timer2.start()"> <input type="button" value="stop" onClick="timer2.stop()"> </p> </form> <script type="text/javascript"> function Counter() { var cnt = 0; this.up = function () { return ++cnt; }; } function Disp( cnt, id ) { var e = document.getElementById( id ); var c = cnt; this.disp = function() { e.value = c.up(); }; } function Timer( disp, time ) { var timerId = null; this.stop = function () { if( timerId ) { clearInterval( timerId ); timerId = null; } }; this.start = function () { this.stop(); timerId = setInterval( disp.disp, time ); }; } var counter1 = new Counter; var disp1 = new Disp( counter1, 'data1' ); var timer1 = new Timer( disp1, 1000 ); //上の3行を1行にまとめると、余計な変数も使わなくてすむ。 var timer2 = new Timer( new Disp( new Counter, 'data2' ), 500); </script>
タイマーのカウンタをリセットする機能を付け加える
reset()が次々と呼ばれていく。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"> <title>TEST</title> <body> <form action="#"> <p> <input type="text" id="data1" value=""> <input type="button" value="start" onClick="timer1.start()"> <input type="button" value="stop" onClick="timer1.stop()"> <input type="button" value="reset" onClick="timer1.reset()"> </p> <p> <input type="text" id="data2" value=""> <input type="button" value="start" onClick="timer2.start()"> <input type="button" value="stop" onClick="timer2.stop()"> <input type="button" value="reset" onClick="timer2.reset()"> </p> </form> <script type="text/javascript"> function Counter() { var cnt = 0; this.up = function () { return ++cnt; }; this.reset = function () { cnt = 0; }; } function Disp( cnt, id ) { var e = document.getElementById( id ); var c = cnt; this.disp = function() { e.value = c.up(); }; this.reset = function () { c.reset(); }; } function Timer( disp, time ) { var timerId = null; this.stop = function () { if( timerId ) { clearInterval( timerId ); timerId = null; } }; this.start = function () { this.stop(); timerId = setInterval( disp.disp, time ); }; this.reset = function () { disp.reset(); }; } var counter1 = new Counter; var disp1 = new Disp( counter1, 'data1' ); var timer1 = new Timer( disp1, 1000 ); //上の3行を1行にまとめると、余計な変数も使わなくてすむ。 var timer2 = new Timer( new Disp( new Counter, 'data2' ), 500); </script>
ボタンが3個に増えてしまったので、このボタンをトグル式にして、
押すたびにstart(), stop(), reset()を呼び出すようにしてみた。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"> <title>TEST</title> <body> <form action="#"> <p> <input type="text" id="data1" value=""> <input type="button" value="start/stop/reset" onClick="button1.push()"> </p> <p> <input type="text" id="data2" value=""> <input type="button" value="start/stop/reset" onClick="button2.push()"> </p> </form> <script type="text/javascript"> function Counter() { var cnt = 0; this.up = function () { return ++cnt; }; this.reset = function () { cnt = 0; }; this.getValue = function() { return cnt; }; } function Disp( cnt, id ) { var e = document.getElementById( id ); var c = cnt; this.disp = function() { e.value = c.up(); }; this.reset = function () { c.reset(); e.value = c.getValue(); }; } function Timer( disp, time ) { var timerId = null; this.stop = function () { if( timerId ) { clearInterval( timerId ); timerId = null; } }; this.start = function () { this.stop(); timerId = setInterval( disp.disp, time ); }; this.reset = function () { disp.reset(); }; } function Button( timer ) { var mode = 0; this.push = function () { switch( mode ) { case 0: timer.start(); break; case 1: timer.stop(); break; case 2: timer.reset(); break; default: } mode = ++mode % 3; }; } var counter1 = new Counter; var disp1 = new Disp( counter1, 'data1' ); var timer1 = new Timer( disp1, 1000 ); var button1 = new Button( timer1 ); //上の4行を1行にまとめると、余計な変数も使わなくてすむ。 var button2 = new Button( new Timer( new Disp( new Counter, 'data2' ), 500) ); </script>
さて、ここまで読んだりする人はいるのだろうか?^^;
ここまで自分が書けるようになったころ、prototypeなんて単語がでてきた。
Timerの関数を見てほしい。これを使うためには、
var hoge = new Timer();
みたいに、宣言するのだが、すればするだけ、オブジェクトが作られる。
しかし、このTimer()関数が、巨大なプログラムの集合体だったら、大量のメモリーを消費してしまう。
そこで、プログラムを部分的に書いて、集合体に見せかけるのが、prototypeだった。
(これは自分が覚えるための解釈であり、実際とはちょっと違うかも)
つまり
function Timer( disp, time ) { var timerId = null; this.stop = function () { if( timerId ) { clearInterval( timerId ); timerId = null; } }; this.start = function () { this.stop(); timerId = setInterval( disp.disp, time ); }; this.reset = function () { disp.reset(); }; }
上を下のように書き直す
function Timer () { this.init.apply( this, arguments ); }; Timer.prototype.init = function ( disp, time ) { this.timerId = null; this.disp = disp; this.time = time; }; Timer.prototype.start = function () { this.stop(); this.timerId = setInterval( this.disp.disp, this.time ); }; Timer.prototype.stop = function () { if( this.timerId ) { clearInterval( this.timerId ); this.timerId = null; } }; Timer.prototype.reset = function () { this.disp.reset(); };
で、自分なりの解釈。
まず、Timer()が呼ばれたときに、その中に目的の関数がなかったら、prototypeにくっついているものを探し、あれば実行する。だからTimer()の関数の中には、余計なものを置かない。必要最低限のものを置く。
初期化のプログラムさえ余計なのだから、最初はそれをコールして終わり。
それから、var timerId = null; のような変数は、関数内であれば、その中の関数はどの関数でもグーロバル変数のように使えるけど、分割すると利用できないので、共有したい変数とかには、this.をつける。
このプログラムでは、オブジェクトが階層化されており、一番下から、Counter, Disp, Timer, Buttonとなっている。本来これらは、同一のレベルで一緒にしてしまったほうが良いのかもしれない。
とにかく、必要な機能を細分化しそれぞれを、部品化しておく。
そこで書き直したのが以下。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"> <title>TEST</title> <body> <form action="#"> <p> <input type="text" id="data1" value=""> <input type="button" value="start/stop/reset" onClick="counter1.toggleMode()"> </p> <p> <input type="text" id="data2" value=""> <input type="button" value="start/stop/reset" onClick="counter2.toggleMode()"> </p> </form> <script type="text/javascript"> var StopWatch = function () { this.init.apply( this, arguments ); }; StopWatch.prototype.init = function ( elementId, intervalTime ) { this.element = document.getElementById( elementId ); this.interval = intervalTime; this.timerId = null; this.counter = 0; this.mode = 0; this.reset(); }; StopWatch.prototype.disp = function ( ) { this.element.value = this.counter; }; StopWatch.prototype.stop = function ( ) { this.timerId && clearInterval( this.timerId ); this.timerId = null; this.mode = 2; }; StopWatch.prototype.start = function ( ) { this.stop(); this.disp(); this.timerId = setInterval( (function (that) { return function() { that.countUp(); }; })(this), this.interval ); this.mode = 1; }; StopWatch.prototype.reset = function ( ) { this.mode = 0; this.counter = 0; this.disp(); }; StopWatch.prototype.countUp = function ( ) { this.counter += 1; this.disp(); }; StopWatch.prototype.toggleMode = function ( ) { switch( this.mode ) { case 0: this.start(); break; case 1: this.stop(); break; case 2: this.reset(); break; default: } }; var counter1 = new StopWatch( 'data1', 100 ); var counter2 = new StopWatch( 'data2', 200 ); </script>
こうすることで、多少すっきり(?)する。
ここで、肝心なのは、start()の中の、
this.timerId = setInterval( (function (that) { return function() { that.countUp(); }; })(this), this.interval );
かもしれない。普通なら下のようしてしまうのだが、これでは動かない!
自分なりの解釈として、「数秒後にcountUp()が動いたときにthisは違うものをさしている」と覚えた。
this.timerId = setInterval( this.countUp, this.interval );
setInterval()の第1引数には、関数名を指定しなければならない。
さてここで、クロージャーというものが登場する
なので上を説明するためには、まず以下の応用であることを、理解しなければならない。
(自分が覚えた手順。)
alert( 'hoge' ); // hoge をアラートする //↑を関数にして実行する function test( str ) { alert( str ); } test( 'hoge' ); //↑の方法だと、どうしても関数に余計な名前(test)をつけなくてはならない。そこで function ( str ) { alert( str ); }('hoge'); //↑これだとだめ!なにかに代入すると実行される。というか、 //関数の宣言ではなく、「式」にしなければならない。 var test = function ( str ) { alert( str ); }('hoge'); //↑式にしたところで、testという変数が邪魔!ところが、"()"で囲むと式になる。 (function ( str ) { alert( str ); })('hoge'); //↑ここで自分なりの解釈。"hoge"をstrに代入し、strを表示する //これを以下のようにinterval()にはめてみる。 setInterval( (function (str) {alert( str ); } )("hoge"), this.interval ); //↑しかし、上の式は、setIntervalが評価されるときに、実行されてしまい。alert()が返す値(なにもないけど)に置き換わるだけで関数ではない。だからこれでは動かない。関数を作ってそれを返すコードにしなくてはならない。
ここでさらに脱線。関数の中に関数を書く。
function hoge( str ) { var xyz = 456; function fuga ( p ) { alert( [str, xyz, p ] ); } fuga( xyz ); } hoge(123); // alertで 123,456,456 が表示される
つまり、str やxyz は、fugaの中から参照できる。ということは、
strに必要な情報を持たせ、fugaで処理させることができる関数を返す(return)ことができる
(function hoge( str ) { return function fuga( ) { alert( str ); }; })(123); //↑の最後の123の部分に自分自身(this)のオブジェクトに置き換えて、 //それを利用した関数を返すには、 (function (that) { return function() { that.countUp(); }; })(this); //↑を1行にして、setInterval()の関数の中に書き込むと、 setInterval( (function (that) { return function() { that.countUp(); }; })(this), this.interval );
さて、今度は、さっき作ったStopWatchのオブジェクトについて、考える。
その中身は、カウントする部分、表示する部分、タイマー部分、ボタン部分から構成されている。
最初は、階層化して作ったけど、上は同一レベルに書き直したもの。
だけど、同一ではおかしい。あるオブジェクトがあり、それがタイマーを持っていて、数を数え、表示する
そのオブジェクトの動作は、ボタンにゆだねる。っての標準的な考え方ではないのだろうか?
まぁ〜自分の思うようにやればよい。
オブジェクト指向の便利な点は、各部品を組み合わせて、新しいものを作るとき、これまでのものを再利用してすばやく作ることができることにもある。でも、今の同一の書き方では、再利用ができる形になっていない。
そこで、部品化しやすいように作ってみる。(結局もとの形にちかくなる。そしてコードは無駄に長くなるのに・・)
部品化して再利用することで、コードは短くなる(はずだった!)
Counterを再利用してます。探してみてね。
これまで、オブジェクトの戻り値は、あいまいだったけど、ちゃんと書こう。(それにしても名前の付け方はダサい。)
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"> <title>TEST</title> <body> <form action="#"> <p> <input type="text" id="data1" value=""> <input type="button" value="start/stop/reset" onClick="counter1.button.nextMode()"> </p> <p> <input type="text" id="data2" value=""> <input type="button" value="start/stop/reset" onClick="counter2.button.nextMode()"> </p> </form> <script type="text/javascript"> // Counter_________ var Counter = function ( ) { this.init.apply( this, arguments ); }; //初期化 Counter.prototype.init = function( startNum ) { this.counter = 0; this.setCounter( startNum ); }; //カウントアップ Counter.prototype.up = function ( ) { this.counter += 1; return this.counter; }; //カウントダウン Counter.prototype.down = function ( ) { this.counter -= 1; return this.counter; }; //値の取得 Counter.prototype.getCounter = function () { return this.counter; }; //値の設定 Counter.prototype.setCounter = function ( val ) { if( 'number' === typeof val ) this.counter = val; else this.counter = 0; return this.counter; }; // Button __________ var Button = function ( ) { this.max = 0; this.bufferFunc = [ ]; this.setFunc.apply( this, arguments ); }; // Counterの機能を継承する Button.prototype = new Counter; Button.prototype.constructor = Button; //個々の状態時の関数の登録 Button.prototype.setFunc = function ( /*, func0, func1 ... */ ) { this.bufferFunc = Array.prototype.slice.call( arguments ); this.max = this.bufferFunc.length; this.setCounter( this.max ); return this.max; }; //状態の設定(実行) Button.prototype.action = function ( n ) { if( 'undefined' === typeof n ) n = this.getCounter(); if( n < this.max ) this.setCounter( n ); var func = this.bufferFunc[ n ]; if( 'function' === typeof func ) { func.call(); return true; } return false; }; //状態の移行 Button.prototype.nextMode = function ( ) { var n = this.getCounter() + 1; if( n >= this.max ) n = 0; this.action( n ); return n; }; //状態の取得 Button.prototype.getButton = function ( n ) { if( 'undefined' === typeof n ) n = this.getCounter(); return { 'counter': n, 'func': this.bufferFunc[ n ] }; }; // Display_______ // this で指定された要素のvalueに設定 var Display = function ( str ) { return this.value = str + ''; }; // Timer________ var Timer = function () { this.setTimer.apply( this, arguments ); }; //時間は、Counterを継承させる Timer.prototype = new Counter; Timer.prototype.constructor = Timer; //繰り返しの設定 Timer.prototype.setTimer = function ( mms, cbFunc ) { this.interval = mms; this.timerId = null; if( 'function' === typeof cbFunc ) this.cbFunc = cbFunc; else this.cbFunc = null; }; //スタート Timer.prototype.start = function ( cbFunc ) { if( 'function' === typeof cbFunc ) { this.cbFunc = cbFunc; } return this.timerId = setInterval( (function(that) { return function() { that.loop(); }; })(this), this.interval ); }; //ストップ Timer.prototype.stop = function ( ) { if( this.timerId ) { clearInterval( this.timerId ); this.timerId = null; return true; } return false; }; //繰り返し行われる。もし関数がtrueを返したら中断できる //ただし、中止した場合、呼び出し元に連絡していない Timer.prototype.loop = function ( ) { var flag = this.cbFunc.call( ); if( flag ) this.stop(); }; //ストップウオッチ__________ var StopWatch = function ( ) { this.init.apply( this, arguments ); }; //Timerの機能を継承する StopWatch.prototype = new Timer; StopWatch.prototype.constructor = StopWatch; //初期化 StopWatch.prototype.init = function ( elementId, interval ) { this.element = document.getElementById( elementId ); //トグルボタンを定義して、StopWatchに加える this.button = new Button( (function (that) { return function () { that.start(); }; })(this), (function (that) { return function () { that.stop(); }; })(this), (function (that) { return function () { that.reset(); }; })(this) ); //ボタンの初期設定で、リセットする this.button.action(2); //Timarの設定をここで行う this.setTimer( interval, (function (that) { return function () { that.countUp(); }; })(this) ); }; StopWatch.prototype.reset = function ( ) { Display.call( this.element, this.setCounter(0) ); }; StopWatch.prototype.countUp = function ( ) { Display.call( this.element, this.up() ); } counter1 = new StopWatch( 'data1', 1000 ); counter2 = new StopWatch( 'data2', 10 ); </script>
時間があったら書き足します。