JavaScriptのデバッグTips

JavaScript Advent Calendar 2010 8日目担当のid:os0xです。
JavaScriptネタは案外範囲が広くて色んなネタがあるので、毎回が楽しみですね。
さて、私はデバッグをネタにしたいと思います。テストではなくデバッグです。誰かが書いたコードをメンテナンスしなきゃー、とか。jQueryプラグイン導入しようとしたけど、なんかうまく動かないーみたいなケースのおはなしです。
JavaScriptデバッグは大変なので、多くの方が日々苦労されていると思います。なぜJavaScriptデバッグが大変なのか少し整理してみましょう。

  1. ブラウザ依存
    まず、なんといってもJavaScriptはウェブブラウザ上で実行されるので、環境が一定ではありません。特定の環境だけを対象にJavaScriptを書くことは滅多にありません。PC向けではIE、FirefoxChromeSafariOperaなど、スマートフォンはほぼWebKitFirefoxOperaが少々ですが、WebKitの中でのバージョン違いはPC以上に複雑で環境を揃えにくい分むしろ厄介です。
  2. 積極的にはエラーを出さない
    変数を宣言なしに使えて、しかもそれがグローバル変数になるなど、JavaScriptは思わぬところにバグが潜みやすい言語といえます。読み取り専用のプロパティに代入してもエラーにならない(代入がスルーされるだけ)ほどです。なんとなく動いてしまったコードが後々にバグとなって帰ってきます。

また、デバッグの難しさとは少し違いますが、テストを自動化し難いため、バグが再発しやすい問題もあります。

さて、今回は実際にバグが発生したコードにどう立ち向かうと良いか、という点にスポットを当てていきたいと思います。
まずはバグを大雑把に分けしてみましょう。

シンタックスエラー

JavaScriptの解釈時にエラーとなる、比較的分かりやすいエラーです。最新のブラウザならそれなりに親切なエラーメッセージで、どの行にシンタックスエラーがあるのか教えてくれるはずです。
ただし、IEのみで起きるシンタックスエラーには注意が必要です。IEの行番号は当てにならないことが多い上に、外部ファイルが複数あるときなど、どのファイルでのエラーになっているのかをIEは教えてくれません。大抵はオブジェクトリテラル( {hoge: 2, fuga: 0,} など)に余計なカンマが付いていることが原因です(IE以外のブラウザは余計なカンマを許容してしまいます)。まずカンマをチェックして、1ファイルずつ読み込んでエラーになるファイルを探し出し、さらにコメントアウトの範囲を変えてエラーになっている箇所を特定するなど、根気のいる作業が必要となります。
ただ、これらはJSLintなどのチェックツールで検出することも可能です。他にも幾つかチェック方法はあり、コマンドラインから JavaScript のシンタックスチェックを行う方法 - #生存戦略 、それは - subtechを参考にどうぞ。

実行時の例外

どのブラウザでも動いていない場合

まずはFirebugを入れたFirefoxか、Google ChromeのDevtools(WebInspector)、SafariのWebInspector、OperaのDragonfly*1などでデバッグしてみましょう。特にWebInspectorには例外が起きたところでbreakするオプションがあるので、まずはこれでbreakしてみましょう。
f:id:os0x:20101207231846p:image
breakするとこのようにその時のコールスタック、ローカル変数が確認できます(左下のf:id:os0x:20101207231845p:image この状態がエラーのたびにbreakする状態です。もう一度クリックするとcatchされてない例外のみでbreakするモードに、さらにもう一度クリックするとbreakしないデフォルト状態に戻ります)。
このbreakした状態で、さらにコンソールを開くとその状態のままJavaScriptを実行することもできます。ちなみに、arguments.callee.toString()を実行すると、自分自身の関数を文字列として取得できます。arguments.callee.caller.toString() と、callerで呼び出し元を遡っていくこともできて、複雑に入れ子になった処理や、圧縮されたコードを読むときなどに重宝します。
f:id:os0x:20101207233147p:image
ある程度バグに目星をつけたら、該当する箇所にデバッグ用のconsole.logなどのコードを埋め込んだり、debuggerキーワードなどを埋め込んでみましょう。ソースコードを直接編集するより、条件付きブレークなどを活用するとよりスマートにデバッグできるはずです(Firebugで元のJavaScriptのコードに手を入れずにdebug用のconsole出力を入れる方法 - 文殊堂)。
さて、これらのツールを活用すれば、FirefoxChromeSafariOperaで正常に動作するようにすることはそれほど難しくないと思います(もちろん内容によりますが、そこまで複雑なことを実装することは多くないかなと)。

IE対策

さて、ある意味本題のIEでのデバッグです。これはどうしても経験に頼るしかないところかもしれません。
まず、前述のシンタックスエラーは無くなっているものとすると、IEでもJavaScriptは実行されているはずです。つまり、適当にalertを入れていけば、どこまで実行されているのか調べることができます。適当にalertを散らばらせて、問題の範囲を絞っていけば問題の箇所を見つけることができます。alertは処理が止まるので、(ループなどに注意すれば)案外デバッグしやすいと思います。
ちなみに、出力をコピーしたいときはalertではなくconfirmを使ったり、処理を止めずに手軽に値を確認するにはlocation.hashに値を入れるなどのテクニックがあります。もちろん、Firebug Liteを使うのも良いでしょう。
さて、問題の箇所を見つけたとして、それをどのように修正するかは内容によりけりです。ただ、IE対応のノウハウはウェブ上で積極的に共有されているので、(適切なキーワードがわかれば)調べれば解決策が見つかると思います。
また、IEもIE8から開発者ツールが付属しており、特にIE9の開発者ツールはかなり強力になっています。 特に有用なのがconsole.dirです。console.dirは渡したオブジェクトのプロパティとその値をすべて表示してくれます。
f:id:os0x:20101208001851p:image
windowのほかにも、documentやdocument.bodyなど、console.dirで解析すればすべてのプロパティ・メソッドを一覧できます。この情報は非常に有用で、初めて見るようなメソッドが色々見つかると思います。それらが解決の糸口になるかもしれません。

まとめ

要するに、FirebugやWebInspectorなどのデバッグツールを使いこなせばJavaScriptデバッグはすっごく楽になりますよ、というお話です。手前で恐縮ですが、http://gihyo.jp/dev/feature/01/devtoolsこれでできる! クロスブラウザJavaScript入門:第2回 完全版:ブラウザとデバッグ環境|gihyo.jp … 技術評論社も参照頂ければと思います。
それでは、引き続き JavaScript Advent Calendar をお楽しみください。
Happy Xmas!

*1:[https://dragonfly.opera.com/app/cutting-edge:title=最新のOpera]でかなり良くなってます

AutoPatchWork更新とSITEINFOのあれこれ

まだChrome版だけですが、AutoPatchWorkを更新しました(AutoPagerizeのセキュリティアップデートとは関係ありません)。
Chrome Web Store - AutoPatchWork
ローカライズして(一部を日本語で表示されるようにしただけですが)、かなり前に作りかけていたSITEINFOの管理機能を載せました(あと、NAVERまとめとTumblrの専用対策)。
こんな感じです。
f:id:os0x:20101024023940p:image
主な機能は

  • 検索
  • ソート
  • 特定のSITEINFOの無効化
  • SITEINFOの編集

"number of successful"はURLがマッチして実際に使われた回数、"number of failed"はURLがマッチしたけどXPathでマッチしなかった回数です。自分がよく使うSITEINFO、まったく使ってないSITEINFOが確認できます。
統計を取り始めたのは今年の2月くらいのアップデートのときなので、人によっては半年以上のデータが溜まっているはずです。
私は何度かリセットしたりしているので、おそらくこの数ヶ月くらいの期間で1回以上使っていたSITEINFOは134件でした。2010年10月時点で、wedataのSITEINFOは2600件以上あるので、実際に使っているのは5%程度といった感じです。
今後は、SITEINFOを追加したりできるようにする予定です。あと、管理機能を切り離しできるようにしてwedata使ってる拡張の共通ライブラリみたいにしても良いかも(AutoPagerizeでも使えるように)。

リアルタイムWebハッカソンに参加してきた

node.jsでWebSocket動かして遊ぼうという素敵なイベント、リアルタイムWebハッカソン : ATNDに参加してきました。
まず、なんといってもめそさんが準備した資料が素晴らしく、大変勉強になりました。資料はリアルタイムWebハッカソンでハンズオンしてきました - 自分の感受性くらいから。node.jsの環境構築から具体的なサンプルの作り方までわかりやすくまとまっています。

で、タイトルのとおりハッカソンなので、上記資料にざっくり目を通してから@yoshikawa_tさんとなにか作ってみようかという話になりました。
ただ、いいアイディアが思い浮かばず、「ゲームとか」と考えたところでという、こちらもnode.js+WebSocket(Socket.IO)で動いているデモアプリを思い出しました。このSwarmationは、右側でお題が与えられて、そのお題の形になるようにマス目を塗り潰すというゲームです。一人一つしか持ちコマがないので、参加者の動きを予想して動かなければいけないというところがポイントです。かなり完成度が高くて、Socket.IOなのでIEでも動くようになっています。
で、これの影響をモロに受けて、マス目を取り合うゲームを考えました。
まず適当なマス目を用意して、クリックすると自分の色に反転させる。反転した状態が1列並ぶと自分が反転させていたマスの数だけポイントが入るという形を考えました。そこに複数人でリアルタイムに反転させあうという要素によって、相手の妨害をするといった戦略がでてきます。さらに自分が反転して置ける数に制限を設ければ、妨害だけでなく上手く協力もしないといけなくなってそこそこゲーム性が出てくるだろうと考えました。

で、できたのがこちら(クライアント側のコード)です。
os0x's
gist: 636371 — Gist

サーバー側はごくシンプルで、クライアントが送った位置情報を他のユーザーにそのまま通知するだけです。あとはそのデータで、クライアント側で現在の反転状況(自分のマス、他人のマス)を把握して、1列揃ったら消してポイント計算という処理を行っているだけです。
黙々と書いた甲斐あって、なんとか時間内にゲームとして成立させることができました。
あ、ちなみになるべくIEでも動くようにそれっぽく書いてあります(改めてみたらaddEventListener使ってたけど、onclickで良いケースだし)。折角のSocket.IOなので、IE6でも動かしたいですから。

反省
  • 私はいつも通りクライアントサイドのJSを書いてただけで、node.jsにあんまり触れなかった…
  • 同じテーブルの方とかともっと協力できればよかった

まあ、今回は3時間もなかったので仕方ないところも。ハッカソンならやはり半日はほしいかなと。

その後はLTがあってなかなか面白そうな話題が出てましたが、コードにコメントつけたりしてていまひとつ集中できず、LTのあたりは リアルタイムWebハッカソン ハンズオン編 - サイト更新停滞ちうっなどに。

全体としてはなかなかまとまった情報を得られなかった node.js についての理解が深められた良いイベントでした。ありがとうございました。

JavaScript連載第16回

クロスブラウザJavaScript入門第16回が公開されています。
今回はthisのお話です。この辺はクロージャとかも絡んでくるので、(記事中にも書きましたが)JavaScript初級から中級への分かれ目になってくると思います*1
まあ、そのせいでソースを理解してもらいにくくなりそうなので、微妙に使いにくさがあるように思えます。実際、クロージャやプロトタイプを使うほど一見綺麗に書けているように見えて自己満足度はあがるけど、それをわかりやすいと思うのは自分だけということになりかねません(もちろん、必ずそうなるってわけではないですが)。
プロトタイプのほうに凝りだすとライブラリ化、つまりなるべく汎用的に動くようなコードを書くようになり、必然的にソースが肥大化していくことがままあります。それはそれで悪いことではないですが、個人的には最小限のコードでコンパクトに実装するほうが好みです(もちろん場合によりけりですが)。

*1:まあ、「thisはargumentsと同じなんだよ」の一言で大体あってる気もするこの頃ですが

Mashup Camp - Chrome Extensions Day開催

Mashup Awards 6関連で#MA6 Mashup Camp - Chrome Extensions / Web Apps Day (未経験者歓迎 JavaScript できれば OK) : ATNDが開催されます。Mashup Awards 6に向けて、参加者でグループを作ってコードを書こうというイベント(いわゆるハッカソン)です。
10月5日の夜にアイディア出し・顔合わせMTG、10月9日に一日コーディングというスケジュールです。
私もアドバイザーとして参加します。なお、同じアドバイザーとして、Googleの北村英志さんも参加します。GDD2010の基調講演やChrome Web Storeのセッションなどで活躍していたあの人です。
もう数日後なので急過ぎるかもしれませんが、Mashup Awardsに興味のある方、Chrome拡張に興味のある方は是非ご参加頂ければと思います。

JavaScript連載第15回

Bootcamp2010GDD2010でこっちに書くのが遅れましたが、クロスブラウザJavaScript入門第15回が公開されています。
前回に引き続きprototypeのお話。このシリーズではあえてクラスとかチェーンといった言葉も避けています。他ではあまり見かけない説明の仕方で、(主観的に)こう説明されていればすんなり理解できるという内容を目指していますが、まだまだ力不足な感は否めません…。
ただ、今回の数学の証明っぽいところは結構お気に入りです。

あれ、全然クロスブラウザしてないですか?気のせいですよ、きっと。

valueOfとtoStringとToPrimitive

valueOfとtoStringメソッドの水深43cmぐらいの深さの話 - 三等兵のもう少し深いお話。コメント欄に書こうかとも思ったけど、最近ブログ書いてない気がしたのでちゃんと記事にしてみる。

まずは問題です。次のコードを実行したときにtrueかfalseのどちらがalertされるかそれぞれ当ててみてください。

var date = new Date();
var date_string = date.toString();
var date_value = date.valueOf();
alert(date == date_string);
alert(date == date_value);
  1. true, true
  2. false, false
  3. true, false
  4. false, true

(難しい問題ではないと思いますが、)この問題の答えは最後に。

続いて、もっとシンプルな問題です。

var d = new Date('2000/01/1');
alert(d+1);

選択肢は次の3つです。

  1. Sat Jan 01 2000 00:00:00 GMT+09001
  2. 946652400001
  3. [object Date]1

1)はDateをtoStringした値、2)はvalueOfした値、3)はObject.prototype.toString.call(d)の値です。


この問題の答えは1です。これはよくハマるところなので、そういうものだと思っている方も多いと思います。
最初の問題もそうですが、DateのtoStringが文字列を、valueOfがシリアル値を返すことを知っていれば、正解できると思います。

余談ですが、DateをtoStringしたときの結果はブラウザによって微妙に結果が異なります。

  • Firefox/Opera
    • Sat Jan 01 2000 00:00:00 GMT+0900
  • Google Chrome
    • Sat Jan 01 2000 00:00:00 GMT+0900 (Japan Standard Time)
  • Safari(Mac)
    • Sat Jan 01 2000 00:00:00 GMT+0900 (JST)
  • Safari(Windows)
    • Sat Jan 01 2000 00:00:00 GMT+0900 (???? (?W?Ž?))
  • IE
    • Sat Jan 1 00:00:00 UTC+0900 2000

IEさんはともかく、Windows版Safari*1

さて、なぜ1になるのかを詳しくみてみましょう。

まず、変数dはDateのインスタンスであり、オブジェクトです。そのオブジェクトに加法演算子(+)で1を渡しています。
加法演算子の仕様はECMA-262の11.6.1で定められています*2
その前半部分は次のようになっています。ここではAdditiveExpressionは左項、MultiplicativeExpressionは右項と読み替えてしまえばわかりやすいと思います*3

  1. AdditiveExpression を評価。
  2. GetValue(Result(1)) を呼出す。
  3. MultiplicativeExpression を評価。
  4. GetValue(Result(3)) を呼出す。
  5. ToPrimitive(Result(2)) を呼出す。
  6. ToPrimitive(Result(4)) を呼出す。

つまり、d + 1のそれぞれ d と 1 をToPrimitiveという内部処理に渡しています。
ToPrimitiveはつまりは型変換であり、ECMA-262の9.1で定義されています。
このToPrimitiveですが、オブジェクト以外の型についてはなにもしません。つまり1は1のままです。1はもともとプリミティブな値なので当然ですね。
さて、もう一方のdはDateオブジェクトです。なのでプリミティブ化処理が行われます。その内容は次のように定義されています。

Object のデフォルトの値を返す。オブジェクトのデフォルトの値は、オブジェクトの内部メソッド [[DefaultValue]] に選択的ヒント PreferredType を渡して取得される。[[DefalutValue]] メソッドの挙動は、全ての ECMAScript オブジェクトの仕様によって定義される。(セクション 8.6.2.6)

今度は[[DefaultValue]]というのが出てきました。上記に書かれている通り、[[DefaultValue]]はECMA-262の8.6.2.6で定義されています。
これによると、[[DefaultValue]]がStringをヒントに呼び出されるとtoStringが、Numberをヒントに呼び出されるとvalueOfを呼びだすと定義されています。toStringとvalueOfを呼び分けているのはここです。ここ大事ですね。
さて、ヒントとはなんでしょうか。と、これはすでにちょこちょこと出てきていて、加法演算子のNOTEには、次のような記述があります。

NOTEステップ 5 と 6 における ToPrimitive 呼出しではヒントを提供しない。全ネイティブ ECMAScript オブジェクトは Date オブジェクトを除き、ヒントの欠如をヒント Number が与えられたように扱う; Date オブジェクトは、ヒントの欠如をヒント String が与えられたように扱う。

[[DefaultValue]]のNOTEにも同様の内容が記述されています。Date オブジェクトだけはStringをヒント、つまりtoStringが呼ばれると定義されていました。

さて、こんな面倒なことになっている原因はなんでしょうか?それは+が、文字列の連結としての加算演算子と数値の加算としての加算演算子という2つの意味を持ってしまっているからです。-、*、/などは数値演算であることが明確なので、そもそもToPrimitiveではなくToNumberが呼ばれ、ToNumberは(オブジェクトに対して)ToPrimitiveをNumberをヒントに呼び出すことになります。

また、加算演算子以外にも、==と!=(抽象的等価比較)についても場合によってヒントなしのToPrimitiveが呼ばれることがあります。具体的には、オブジェクトとプリミティブ値が比較された場合です。

最初の問題に戻ってみましょう。

var date = new Date('2000/01/1');

var date_string = date.toString();
// Sat Jan 01 2000 00:00:00 GMT+0900
alert(date == date_string);

dateはオブジェクトで、date_stringは文字列なので、11.9.3 抽象的等価比較アルゴリズムのステップ21からToPrimitive(date)が呼ばれ、その中でtoStringが呼ばれます。結果、dateとdate_stringは一致します。

var date_value = date.valueOf();
// 946652400001
alert(date == date_value);

dateはオブジェクトで、date_valueは数値なので、同じく11.9.3 抽象的等価比較アルゴリズムのステップ21からToPrimitive(date)が呼ばれ、その中でtoStringが呼ばれます。dateは文字列になり、date_valueは数値なのでこちらは一致しません。
というわけで、最初の問題の答えは3でした。

おまけで、以前話題になったwtfjsから、こちらを読み解いてみると面白いと思います。

[] == ![] // true

http://wtfjs.com/2010/02/15/careful

*1:windows版リリース当初からバグッたまま

*2:ホントは5th editionも参照するべきなんだけど、ざっと見た感じ特に変更はなさそうなので割愛

*3:左項、右項って日本語はなさそうですが、伝わると思います