javascriptのカスタムイベントを作ろう!
デフォルトで用意されているイベントは、基本的にはブラウザがfireすることで要素に登録されたイベントリスナーが呼ばれるわけなんですが、このイベントのfireを開発者側でも行えるワケです。これはテスト用途での使われることが多いみたいです。
というわけでそのイベントのエミュレートと独自イベントの合成を学んだので覚書。サイ本じゃ合成イベントなんて言われてるけど、googleで検索するとカスタムイベントって言われるほうが多いみたい。
おおまかな流れ(IE除く)
- イベントオブジェクトの作成
- イベントオブジェクトの初期化
- イベントのディスパッチ(発火)
という感じ。発火の前にはaddEventListenerで任意の要素に、イベントハンドラを登録する必要があります。
イベントオブジェクトの作成
var event = document.createEvent(type);
引数のtypeはイベントタイプを表します。イベントタイプには、onclickやonkeydownなどの詳細なタイプではなく、DOMLevel2EventsまたはDOMLevel3Eventsのイベントモジュールとして定義される以下の大きな分類を指定します。
- HTMLEvents(HTML要素に関するイベント、onchange,onfocus,onloadなど)
- MouseEvents(マウスに関するイベント、onclick,onmousedownなど)
- MutationEvents(要素の変更に関するイベント、DOMNodeInsertedなど)
- UIEvents(DOMFocusIn,DOMFocusOutなど、正直よくわからない)
※上記はDOMLevel2Eventの分類です。DOMLevel3EventsではkeybordEventsが入ってきたり、onloadがUIEventsモジュールに入ってたりします。
イベントオブジェクトの初期化
とにかくイベントオブジェクトを作成できたら、そのイベントオブジェクトを初期化します。これもイベントモジュールごとに初期化用メソッドがあるので、それを使用します。ここでイベントハンドラに渡す引数となるイベントオブジェクトを初期化するわけです。
event.initEvent(type, bubbles, cancelable)
上記はHTMLEventsの初期化メソッドです。typeにはイベントの名前(onfocus,onloadなど)を付けます。もちろん自分の独自の名前だってつけられます。bubblesはイベントを伝蟠させるかどうかのbool値を、cancelableにはpreventDefaultでキャンセルできるかどうかのbool値を指定します。
initEventはHTMLEvents用ですが、MouseEventsの場合はinitMouseEventを、UIEventsの場合はinitUIEventを、MutationEventsにはinitMutationEventを指定します。
上記の各メソッドごとに渡す引数が違ってきます。例えばMouseEventsであれば、イベントハンドラに引数としてMouseEventオブジェクトを渡す際に、screenX,screenYなどの情報が必要になるので、この初期化メソッドにそれらの情報を渡します。
各種イベント初期化メソッドについてはこちら(正直書くの面倒になってきた)
https://developer.mozilla.org/ja/DOM/event.initEvent
https://developer.mozilla.org/en/DOM/event.initUIEvent
https://developer.mozilla.org/en/DOM/event.initMouseEvent
イベントのディスパッチ(発火)
初期化ができたら、イベントを発火させたい要素のdispatchEventメソッドを呼び出します。このとき、このメソッドの引数として、さっき初期化したイベントオブジェクトを渡します。
var elm = document.getElementById("test"); elm.dispatchEvent(event);
あとは通常のイベントフローと同じ。イベントオブジェクト初期化時にわたしたtypeにイベントハンドラが登録されていればそのイベントハンドラが実行されます。
実際にコード書いたほうが流れ分かりやすいかも
"custom_event"というそのまんまのカスタムイベントを作成してみます。
<span id="emulator" style="display:inline-box;border:1px solid black;background:gray"> emulate event </span> <script> var elm = document.getElementById("emulator"); //カスタムイベント(custome_event)にイベントハンドラ登録 elm.addEventListener("custom_event", function(){ alert("custom event fire!!"); }, false); //要素クリック時にカスタムイベントを発火させる elm.addEventListener("click", function(){ //カスタムイベントの作成・初期化 var customEvent = document.createEvent("HTMLEvents"); customEvent.initEvent("custom_event", true, false); //fire!! this.dispatchEvent(customEvent); }, false); </script>
ちょっと待って、addEventListener使ってるってことは…?
そうです!!可愛さ余って憎さ1万倍!みんな大好きInternetExplorerさんはもちろんcreateEventなんてメソッド持ってません!はやく滅びろ!!バルス!!!!
ということでスーパー面倒ですが、IEはcreateEventなどに相当する独自メソッドを使いましょう。*1
IEでのイベントオブジェクトの作成
var event = document.createEventObject();
これでIE(before version9)でもイベントオブジェクトを作成できます。
IEでのイベントのディスパッチ(発火)
var elm = document.getElementById("test"); elm.fireEvent("onclick", event);
これでイベントを発火させることができます。第一引数にはonclick,onloadなどのイベントタイプを、第二引数にはイベントオブジェクトを指定します。
ここで面倒なのは、実はIEではこの方法を使う限りカスタムイベントの作成はできません。上で"custom_event"というカスタムイベントを作成しましたが、それはIEでは出来ないということです。既存のイベントタイプを使うという方法しかありません。IEにおいては、カスタムイベントを作るというよりも、テスト用のイベントエミュレータという位置づけっぽいです。
テスト駆動JavaScriptは今年最後のお得な買い物だった件
一部で好評を博している書評。
まだあと3週間ほど2011年を残しながらも今年最後と断言するあたり、僕の買い物下手っぷりが露呈しているわけです。amazonで異常なくらいレビューが良くて安い椅子なんてもう買わねえ…(遠い目
- 作者: Christian Johansen,長尾高弘
- 出版社/メーカー: アスキー・メディアワークス
- 発売日: 2011/11/25
- メディア: 大型本
- 購入: 19人 クリック: 331回
- この商品を含むブログを見る
というわけでこの本を買いました。
タイトルだけ見るとjavascriptにおけるテストしか書いてないんじゃねえかとも思ったのですが、そんなことは全くありません。
javascriptは、嫌がらせがプロダクトテーマである某クソブラウザを始めターゲットプラットフォームが幅広いにも関わらず、「テスト駆動開発」というものが浸透してなくね?なんかおかしくね?ということを本書は提言しています。javascriptでテスト駆動開発をうまいことできるようにして、壊れにくいアプリケーションを作れる自信と力がつくようにしようというのが本書のコンセプトらしいです。
全体は4部構成になっていて、1部はテストとは、テスト開発駆動とはどういうものなのか、javascript用のテストフレームワークにはどういうものがあるかという感じのテスト駆動開発初心者向けの解説。
2部はjavascript自体の解説。文法とかのかったるい内容はすっとばしてアクティベーションオブジェクトなどを含めたスコープの詳しい動作解説、カプセル化などのオブジェクト指向的な組み方、さらにはEcmaScript5の機能解説まで、という結構お得な内容。javascriptパターンとかぶる部分も結構あるけど。
で、一番嬉しいのが3部で、ここからjavascriptで実際にテスト駆動開発してみよう!ということで小さなプロジェクトを開発していくのだけど、その題材がajax、comet,node.jsと、ライブラリなしで組むのは気が進まないような部分を取り上げてくれてます。今まともにnode.jsのこと書いてくれてる本ってあんまりないんじゃないでしょうか?まぁとんでもないスピードでバージョンアップしてるから陳腐化しやすすぎて書けないんだろうけども。ライブラリ一切なしで一からこのような面倒な部分を解説しながら組んでくれるのはありがたいです。テスト開発駆動を実践形式で覚えながら、さらにjavascriptの割と大事なところや今まさに旬な部分まで解説してくれちゃう辺りがお得感満載です。
4部はまぁなんかテストのパターンについて書いてあるらしいッス。読んでないのでよく知りません。え?急に適当になってるって?いや別にもう書くの飽きた訳じゃないですからね。
まぁそんなこんなで買いです。やっぱり飽きてるじゃねぇかって?まぁそうです。
prototypeの正体
prototypeってものを本で読んだだけでわかったつもりになっていたけど、いざ使ってみたら勘違いばかりでダメダメだったので覚書。やっぱ何か覚えるときはコードの写経だけでもしといたほうがいい気がする。
動かないコード
こんなコードを動かそうとして失敗した。
var proto = { prop:"aaaaaa", alert:function(){ alert(this.prop); } }; var base = function(){ this.prototype=proto; }; var test = new base(); test.alert();
で、こんなエラーが出る。
test.alert is not a function
明らかに
this.prototype=proto;
が怪しいので、こいつをコメントアウトして、さらにbaseコンストラクタの前で
base.prototype=proto;
をしてやる
var proto = { prop:"aaaaaa", alert:function(){ alert(this.prop); } }; var base = function(){ //this.prototype=proto; }; base.prototype=proto; var test = new base(); test.alert();
こうすると test.alert(); が成功する。
コンストラクタの中でprototypeの指定までできたらスッキリしていいなぁと思ってエラーになってるthis.prototype云々という行をいれたけど、案の定ダメだった。このコンストラクタの宣言の前でprototypeとなるオブジェクト(proto)を定義してあるからよさそうな気もするんだけどなあ。
prototypeと__proto__
var a = function(){this.x = "prop x"};
console.dir(a);
このコードを実行してみる。console.dirで以下のような値が出力された。
「prototype」と「__proto__」というなんか怪しげなプロパティがでてくる。こいつらは何なのか?
var a = function(){this.x = "prop x"};
var b = function(){this.y = "prop y"};
b.prototype = new a();
console.dir(b);
var objB = new b();
console.dir(objB);
今度はこんな感じで2箇所にconsole.dirを設置してみる。
1つ目は関数オブジェクト b に対するもの…(1)
2つ目はbをコンストラクタとして生成されたobjBというオブジェクト…(2)
(1)では「prototype」にaが入り、__proto__は空の関数オブジェクトが格納されている。(2)では「prototype」というプロパティはなく、__proto__にbの「prototype」として追加した a が入っている。どういうこと?
prototypeの役割
そもそもprototypeがしてくれるのは、実行時のプロパティまたはメソッドの検索。これは多くのjavascript入門本なんかにも書かれている。が、ここが紛らわしい(とこだと個人的に思う)。
javascriptは実行時に以下の順序でプロパティを探す。
1. そのプロパティが属するオブジェクト
2. そのオブジェクトの__proto__プロパティとして定義されている__proto__オブジェクト
3. __proto__チェーン上の__proto__オブジェクトを再帰的に検索
4. 最上位のObjectの__proto__オブジェクトにもない場合は undefined を返却
つまりプロトタイプチェーンの実体は__proto__にセットされているオブジェクトを再帰的に検索しているもの。上記の(2)のケースでは objB の__proto__には a が入っていたので、objBのプロトタイプチェーン上には a がいるということになる。なのでalert(objB.x)などとやったら "prop x" という文字列が出力される。
__proto__の役割はわかったけど、じゃあ「prototype」プロパティとはなんなのか?それと__proto__との関係はどうなのか?
コンストラクタによるオブジェクト生成の瞬間
newと関数を組み合わせた形式 new SomeFunc(mayHaveArguments); でオブジェクトが生まれるのですが、そのとき、次の手順に従います。
- 処理系が、プレーンなオブジェクトをヒープにアロケートする。
- コンストラクタ(として使われる)関数の呼び出し環境(Callオブジェクト、スタックフレームに相当)のthisとして、今アロケートしたオブジェクトをセットする。
- コンストラクタ関数のコードが実行される。
- thisに入っていたオブジェクトが返される。
大筋はこういうことですね。おっと、大事なことが抜けている。そう、__proto__プロパティのセットです。
●生成したオブジェクトの__proto__プロパティとして、そのコンストラクタ関数のprototypeオブジェクトをセットする。
このことから、オブジェクト生成直後の__proto__オブジェクトは、コンストラクのprototypeオブジェクトと一致することになります。
http://d.hatena.ne.jp/m-hiyama/20050909/1126235062
まぁそういうわけなんです。
「prototype」プロパティはすべての関数オブジェクトがもつプロパティ。なのでそれを基にして上記のような手順でprototypeが__proto__に設定される。
ちなみにこの__proto__というのはjavascriptの独自実装で、ES3には含まれていない。ES5で__proto__と同価の Object.getPrototypeOf() というメソッドが追加された、って最近話題の「パーフェクトjavascript」を立ち読みしたら書いてありました。http://www.ibm.com/developerworks/jp/web/library/wa-ecma262/
javascriptのin演算子
for(var i in obj){ … }
みたいな感じであるオブジェクトの中をforで走査する方法は知ってた。が、なんとなくこのforに使われてる「in」は何モノなのかが分からずモヤモヤしてたんだけど、このinて演算子なんだと今知った。
2011/10/11追記
http://d.hatena.ne.jp/teramako/20110304/p1
全然in演算子じゃなかったみたいです。in演算子はin演算子で存在するけど、for(... in ...)は特殊構文とのことです。
in演算子
in 演算子は、指定されたプロパティが指定されたオブジェクトにある場合に true を返します。構文は以下のとおりです。
propNameOrNumber in objectName
https://developer.mozilla.org/ja/JavaScript/Guide/Operators/Special_Operators#in
以下のような感じで確かめることができる。
var a={b:1}; alert(b in a); //true
ただ実用性はどうなんだろうか?
var a={b:1}; if(a.b){ alert(true); }
ってしたほうが明らかに分かりやすいからなぁ…inのほうが文字数少なくて済むってのはあるけど。