2006年 02月09日(Thu) [長年日記]
_ [プログラム]Javascriptでハマる(解決編)
何か昨日の日記はこれとバトンのせいで、ここ4年間のなかで最も長い日記だったような気が。 それはともかく昨日の続き。
さて、昨日はeachを使った途端に動かなくなっちゃうと言う話でしたが、これはeachの引数としてeachで呼び出す関数を渡してるんですが、これがC++の関数オブジェクト等とは性質が違うからです。
説明だけではなんですから例を挙げましょう。まずは昨日の例に新しいクラス定義を追加してみます。
var Counter2 = Class.create(); Counter2.prototype = { initialize: function() { this.count = 0; this.observers = new Array(); }, setCallback: function(callback) { this.callback = callback; }, addObserver: function(observer) { this.observers.push(observer); }, notifyObservers: function() { for(i = 0; i < this.observers.length; i++) { this.observers[i].update(this); } } };
こんな感じ。Counterクラスとの違いはcountUpが無いのと、その代わりに任意のコールバック関数をセットするsetCallbackがあるという点です。
で、インスタンス生成以降の部分は以下のように書き換えます。
var a = new ElementView('count1'); var b = new ElementView('count2'); var c = new Counter(); var d = new Counter2(); c.addObserver(a); d.addObserver(b); d.setCallback(c.countUp);
HTMLも上に合わせて書き換えましょう。
<html> <script type="text/javascript" src="./prototype.js" charset="UTF-8"></script> <script type="text/javascript" src="./test2.js" charset="UTF-8"></script> <div id='count1'>0</div><div id='count2'>0</div> <input type='button' value='count up 1' onclick='c.countUp()' /> <input type='button' value='count up 2' onclick='d.callback()' /> </html>
実際の例はこちら。ボタンが2個あってcount up 1を押すとc.countUpが呼ばれ、count up 2を押すとd.callbackが呼ばれる仕組み。
で、動かしてみると分かるんですが、count up 2を押すと下側の数字がカウントアップされていきます。あれ? d.callbackってc.countUpがセットされているだからcがカウントアップされるのでは?
そうなのです。Javascriptでは関数を呼び出したオブジェクトがthisに設定されるのであって、元のオブジェクトには関係ないのです。だから上の例でもcでは無く、dがカウントアップされるのは正しい動作と言えます。
でもそれだと困る事もあるんですよね。私はcのcountUpを登録したんだから、cがカウントアップして欲しいって場合だってあると思います。ここでは触れませんが特にイベントに対するコールバック等では良くあるケースだと思います。
で、私も上のような状況にハマってしまって悩んでいたんですが、これに対する回答がprototype.jsの中に存在しました。それがbindです。
bindを使うとある関数のthisを固定(束縛)することが出来ます*1。まあ例を挙げたほうが分かりやすいでしょう。先ほどの「d.setCallBack(c.countUp)」の部分を次のように書き換えます。
c.addObserver(a); d.addObserver(b); d.setCallback(c.countUp.bind(c));
実際の動作を見れば、count up 1でも、count up 2でもcがカウントアップしているのが分かると思います。これは「c.countUp.bind(c)」でc.countUpのthisをcに固定(束縛)しているからです。
さて最初の問題に戻りましょう。実はeachを使った途端に動かなくなってしまったのも、eachに渡した関数のthisが適切に設定されていなかったせいなのです。したがってCounter.notifyObserversを以下のように書き換えれば解決します。
notifyObservers: function() { this.observers.each( function(value, index) { value.update(this); }.bind(this) ); }
*1 かなり大雑把な説明ですが(^^;