組込み系でクライアントサイドしたいJavaScript素人へのJavaScript開発入門の補講

世の中にはJavaScript入門の記事や本なんて山ほどあるし、ググればツールの情報も色々と出てくる。プログラマなら例えJavaScriptの素人であっても何とかなるぐらいのネタはある訳で、今さら同じようなことを書いても無駄だろう。

なのでこのエントリでは、

  • 組込み系プログラマとして今まで静的型付けの言語(CとかC++とか)でガリガリ書いてきた。
  • で、何故か業務命令でクライアントサイドJavaScriptすることになった。
  • う〜ん、言語知識から文化、各種ツールまで何のノウハウもないよぅ……。
  • 周囲に頼りになる人もいないし(みんな静的型付けの言語でゴリゴリ……)。

――という人が自分で色々と調べながら作業を進めていく時に補助として使うことを目的として断片的に色々と書いておこうと思う。なので他言語の経験者向け。

というか全くもって自分自身の話じゃないか。まあ半ば趣味でJScriptHTAを使っていたのだけど、仕事でクライアントサイドJavaScriptとなると色々違うものです。

なお私は(世の中の分類としては)組込み系の人で、製品開発絡みでJavaScriptする立場として短期間色々と動いていたいうこともあり、一般的なWeb開発とは微妙に違いがある文化圏の話が混ざっている。タイトル通り組込み向けですな。

C/C++プログラマJavaScriptを使うときに注意すべきことの一例

JavaScriptには割と知られているようで実は知らない人*1も意外といる重要な特徴がある。

それは:

  1. Cや古典的なC++とよく似たシンタックスを持っているけど中身は全くの別物。
  2. Cとは別の方向にて、C言語並みに使用者(プログラマ)の器量が問われる言語。
  3. 上記 (1) (2) より、CやC++のイメージを引き摺ったままコードを書くと痛い目に遭う。

なので言語本体の勉強はキッチリとやっておきましょう。

よくある例としては:

  • 整数なんて存在しない。アレは全て浮動小数だ。*2
  • 変数のスコープはグローバルか関数内。ブロックスコープなんてものは存在しない。
  • varを付けなかった変数は、例えそれが関数内で宣言したものであっても暗黙のグローバル変数になる。
  • クライアントサイドJavaScriptの世界ではグローバル変数は本当にグローバル。ブラウザオブジェクトもライブラリも自分のコードも全て同じグローバル空間に存在する*3
  • グローバル変数の名前が他のライブラリのそれと衝突してもエラーは起きない*4。頭を掻き毟りたくなるような不具合が起きることはある。
  • 巻き上げの問題があるので、変数定義のスタイルで回避することが多い。
  • `{' の位置によって挙動が変わる場合があるので、大抵は行末に置くスタイルが多い。
  • コンストラクタを普通の関数として実行できてしまい、それによって大抵は妙な不具合が起きるので、命名のスタイルで回避することが多い。
  • 行末に `;' を書き忘れてもエラーにならずに実行できる。その代わり、柔らかい脇腹を突かれるとは(ry
  • `==' か `===' か、又は `!=' か `!==' か。

JavaScriptを書いている人なら知っている基本的なことばかりだけど、他言語の経験者がよく勉強しないままに突き進むと見事に地雷を踏んでしまうというか、ちょうど踏みやすい位置に地雷がある言語仕様というか。

いやね、ある関数のfor文でインクリメント用の変数としてvarを付けずにiとか使っていて、他の関数でもiとかjとかfor文で使っていて且つfor文の中で関数呼び出ししていて(つまり実質的に多重ループで、且つ同じインクリメント変数を使っている状態……)、しかしクライアントサイド全体の挙動としては全く問題ないとか、「これどうやって直せばいいのよ?」と途方にくれかけたのが私だけとか、どないせいちゅうねん……。

地雷以外の言語機能としては、以下に関しては要勉強だろう。

  • プロトタイプベースのオブジェクト機能。C++とは随分と勝手が違うので。
  • 関数が第一級のオブジェクトなこと、無名関数の存在。もっとも関数ポインタやファンクタを使っていた人なら馴染みやすいと思う。
  • クロージャ。まあアレはある意味オブジェクトの仲間だと考えれば……。
  • Object。いやArrayやStringも大事だけど、Objectはデータの構造化だとかオプション引数の代替だとかで意外とリテラル表記を使うので。
  • 正規表現。まあ組込み系といえどもテキスト処理でPerlRubyPythonだとかを使っている人はそれなりにいるので、心配無用かも。*5

モダンなJavaScriptのライブラリを使ったり中身を読んだりする場合、大抵ユーザがこのあたりの機能を知っているという前提でライブラリが実装されている。なので使う側/読む側も勉強しておく必要がある。

JavaScriptの言語部分の参考書籍

言語仕様に関しては、その昔は『JavaScript 第5版』ぐらいしか良書が無かったように思う。

JavaScript 第5版

JavaScript 第5版

今なら『パーフェクトJavaScript』を薦める。『JavaScript 第5版』も悪くはない(むしろ良書だ)けど、如何せん重厚すぎるのでJavaScript初学者向けとは言い難いしECMA-262 第5版に対応していない。

パーフェクトJavaScript (PERFECT SERIES 4)

パーフェクトJavaScript (PERFECT SERIES 4)

一方で『JavaScriptクイックリファレンス 第5版』は初学者でも十分使い物になると思う。でもES5に追従していない所が微妙に痛い。

JavaScriptクイックリファレンス

JavaScriptクイックリファレンス

JavaScript』も含めてもうじき第6版の日本語訳(ES5対応)が出るので、それを待った方が良いかもしれない。

作法本は必須。『JavaScriptパターン』は万人向け。ちょっと過激で割り引いて読むべき部分もあって好き嫌いは分かれそうだけど『JavaScript: The Good Parts』も良い本。うん、2冊とも読め。

JavaScriptパターン ―優れたアプリケーションのための作法

JavaScriptパターン ―優れたアプリケーションのための作法

JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス

JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス

ちなみに私の場合、2009年頃にJScriptを触りだした際にはMozillaJavaScriptリファレンスを参照しつつ『JavaScript: The Good Parts』を真似てコードを書き、時々『JavaScript 第5版』を読んで補完していた。確かクライアントサイドJavaScriptでのモジュール化/ライブラリ化の方法は『JavaScript 第5版』から学んだはず。

周辺知識

実際に開発する際は言語本体とは別に以下の知識も欲しくなってくる。

  • HTML/XHTMLXML
  • DOM
  • ブラウザ
  • HTTP
  • 各種ライブラリ
  • 各種ツール

で、まあごく稀に下のレイヤーを意識する必要もあるような気がする。例えばAjaxで非同期通信している時に物理的に通信が遮断されたら……*6

コーディングルール

JavaScriptの文化には「言語仕様のマズイ部分を作法でカバー」という面があるように思う(いやまあCとかC++でも同じだけど)。なので可能なら既存のオープンなJavaScript用コーディングルールを参考にした方がよい。

日本語訳があるものとしては以下が参考になる。

JavaScript Coding Guidelines for Mac OS X」というのもあったけど、行方不明?

コードは意識してキレイに書くこと。ミニファイツールにかけない場合は特に。何しろクライアントサイドJavaScriptのコードは外部から丸見えなので……。

ところでGoogleのコーディングスタイルだとインデントはスペース2個なのだけど、変数の定義のみ次のようにスペース4個でインデントして書きたくなる。

var foo,
    bar,
    baz;

このコードをJSLintで「indent: 2」の設定でチェックすると、barとbazの定義位置が正しくない旨のメッセージが出る。何か回避方法はないだろうか? JSHintなら何とかなるのかなあ?

テキストエディタ

大々的に開発するならIDEを使うべきだろう。この辺りは情報を集めて試用して決めるべきだと思う。

とはいえ、そこまでする気力が無い場合もある訳で……。

もし他の言語での開発用にIDEを使っているのなら、そのIDEJavaScript編集機能を使うのが手っ取り早い。プラグインを入れることで対応できる場合もある。Visual StudioNetBeansEclipse辺りはJavaScriptの開発にも使用できるようだ。

Emacs使いならjs2-modeが鉄板だろうか? Vim使いも幾つかプラグインがあるらしいけど、私の場合は手抜きしてftpluginにて「JavaScript編集時はスペース2個でインデント」ぐらいしか手を加えていない――そういえばJavaScriptのキーワードファイルを辞書登録してたなあ。

他のエディタは知らない。

Lint

CやC++コンパイルチェック*7するように、JavaScriptでは実際に動かしてみる前にLintチェックをするべきだ。出来れば小まめに、例えば関数を追加する度ぐらいの頻度で。

JavaScriptの処理系は基本的にインタプリタだ。もちろん内部では高速化の為にJITコンパイルしてたりする*8けど、ユーザから見れば「明示的なコンパイルが不要で、実行時にソースを読み込んで処理している」という点でインタプリタだ。

なので世の中のスクリプト言語と同様に「実際に動かしてみないと分からない」という世界だ。もっともCで書いたプログラムだって「実際に動かしてみないと出てこないバグ」は山ほどある ;) まあCやC++と違って文法レベルでの間違いも実行しないと分からないので、「実際に動かしてみないと分からない」の範囲が広いとでも言うべきか。

動的型付けのスクリプト言語での開発が主流な世界では、関数/メソッド単位でのテストが重視されているように思う*9。TDDほどではなくても、テスト・フレームワークを導入してメソッドやモジュールに対するテストを書き、繰り返しチェックする。文法レベルの問題を洗い出すには何らかの形でコードを実行する必要があるので、できればアプリやライブラリ等の実装と並行して単体テストレベルのテストを実装・実行した方がよいだろう。そうすれば予めテストを通じて最低でも文法レベルのミスが取り除かれた状態で*10、実際にスクリプト本体を実行して動作をチェックすることができる。

この点はJavaScriptも同じなのだけど、そもそも言語仕様的に落とし穴が多い。なのでテストの前にLintをかけるべきだ。頻繁にLintをかけることでコンパイルチェックと同様の文法チェックの効果が得られる(チェックされる内容はコンパイルチェックよりも広い)。

JavaScript用のLintは幾つかあるけど、手軽さで言えばJSLintないしJSHintだ。Webサイト上から利用できるのみならず、ローカルのコマンドライン上でチェックを実行することもできる。JSLintやJSHint本体はJavaScriptで書かれた関数なので、その関数を呼び出す仕組みを用意してコンソールで動作する処理系で実行すればよい。

例えばWindowsで開発しているならWSH JSLint Runner等を使ってcscriptでチェックを実行できる。ネット上にはRhino、Node.js、PhantomJSなどで実行する方法の情報も見つかる。コンソールからチェックできるようにしておけばエディタとの連携も楽になる。

個人的な好き嫌いで言えば、JSLintの方が好みだ。割とカッチリしたコードを書いてきた人は受け入れやすいように思う。使用するライブラリの対応度やオプションの柔軟性からいえばJSHintの方が良いのだけど、JSHintのデフォルト設定が甘めなのがネックだ。厳しくチェックするためにオプションを幾つも書かなくてはならない。Lintの類は「デフォルト値は厳しめだけどオプションで甘くできる」の方がよいと考えている人なので、ちょっと使い難い。

ただJSLintは作者の好みの押し付けが強いツールなので、好き嫌いが分かれる。肌に合わないと思ったらJSHintやその他のツールを使ってみたほうがよいだろう。

ミニファイ

クライアントサイドJavaScriptでは、記述したソースコードがそのままHTTPを介してサーバからクライアントに転送され、ブラウザのJavaScript処理系に読み込まれる。これはつまりソースコードが大きいほどHTTP通信やJavaScript処理系の読み込み処理の負荷が大きくなるということだ。組込み機器ではサーバ側となる機器の通信負荷も気になる。可能ならソースコードを小さくしたい所だろう。

しかしだからといって最初からコメントを書かないとかインデントをしないとか昔懐かしBASICの頃のような暗号めいた短い名前を使うとか、ハッキリ言って自殺行為だ。

JavaScript界隈にはミニファイ用のツールがあるので、ソースコード自体はコメント付きで丁寧に記述しておき、後でツールでミニファイするようにするべきだろう。ツールによっては変数名を短くできるものもあるけど、製品の品質保証の面で不安があるならそこまでする必要はない。コメントや余分な空白の除去だけでも効果がある。

ミニファイ用のツールを選択する場合、組込み系に絡む事情としては以下が考えられるだろう。

  • 文字コードを指定できること。
    • 普通のWeb開発では最近はUTF-8が主流らしいけど、組込みの世界ではそうではなくて、それが影響してSJISを使うとか十分あり得る。*11
  • 複数のプラットフォームで動作すること。
    • 組込みLinuxだと「ツールチェーンがLinux用なので、機器に書き込むROMイメージをビルドするシステムをLinux上に構築する」という可能性がある。そこにミニファイの工程を組み込みたい場合、Linuxで動作するツールでなくてはならない。
    • 一方で開発者側は、バージョン管理システムを使って1ヶ所でソースを管理しつつ担当箇所を個別のPCで開発するだろう。その場合、WindowsMacで作業している可能性が高い。各担当者が個々にミニファイを実行したい場合、WindowsMacで動作するツールでなくてはならない。

この辺りを考慮すると、ツールの候補としてはClosure CompilerYUI Compressorあたりになるかと思う。

ミニファイツールを使うなら、導入は早めにした方がよいだろう。ソースコードを加工することになるので、単体テストを含めたテスト工程にてミニファイしたコードをテストするべきだ。私の場合、ミニファイの検討開始が遅かった為にこの点が引っかかってしまい、ミニファイを諦めることになった。

ブラウザ

世の中には山ほどブラウザがあり、それぞれサポートしている機能が異なる。とても全てには対応できないし、対応するブラウザが多いほどテストも大変になる。

通常のWeb開発でも品質は重要だけど、組込み機器のWeb管理画面のようなものになると品質の要求は更に高くなる。出来が悪いとネット上で噂になるし、時として売れ行きに影響することもあるだろう。稀にブランド・イメージに傷をつけてしまうこともある。

加えて、仮に不具合があって修正版をリリースした場合、一般的なWebサービスと異なりユーザ自身にアップデートしてもらう必要がある。修正版公開の周知は大変だし、ユーザからすればアップデートは面倒な(そして人によっては難しい)作業だ。自動アップデート機能はサービス自体の準備に手間がかかるし、アップデートサーバの維持・管理等で継続的なコストが発生する。

なので初回リリースの時点で製品としてそれなりに完成された高品質な状態になっていることが望ましい。その為には設計や実装段階での品質確保だけでなく、テストにもコストを割く必要がある。

品質要求とテストのコスト。両者の折り合いから、どうしてもサポート対象のブラウザを絞らざるをえない。もちろん積極的に自動化を進めることによって間口を広げることは可能なのだけど、そこまでできる人材がいない/そこまで人が育つ前に開発が完了してしまう可能性の方が高いだろう。

対象をPCに限定した場合、最近のブラウザ・シェアから判断するに最小構成はこんな感じだろうか。

OS ブラウザ 備考
Windows Internet Explorer 6 正直、捨てたい……個人向けなら諦めてもいいよね?
Internet Explorer 7 正直、捨てたい……。
Internet Explorer 8 どうしてもWindows XPに対応したい場合の最低ライン(希望)。
Internet Explorer 9 HTML5存分に使いたいなら9よりも前は捨てるべき。
Mozilla Firefox 基本的に最新版に対応。ESRへの対応は要求次第。
Google Chrome 基本的に最新版か次期ベータ版に対応。
Mac OS X Safari Macをサポートするなら必須。

Operaをサポートするか否かが悩むところ。

え、他のOS? んなもんサポート外だ――といいつつもiOS(というか正確にはOSじゃないけどiPad)ぐらいは対応したいのだけど。残念ながらスマフォもガラケーも対象外です……。

テスト工程は別として、開発段階で手を抜くとしたらMacSafariだけでなくWindowsSafariを併用するぐらいか? 動作チェックにてOSの違いが影響しない部分はWindows版を使い、それ以外はMac版でチェックする。

――そう、面倒なことにブラウザの違いだけでなくOSの違いも考慮しなくてはならない(例えばフォント回りとかTCP/IPプロトコルスタックとか)となると、テストする組み合わせが足し算じゃなくて掛け算で増加する(可能性が高くなる)というヤヤコシイ事態に突入してしまう。

従来のメーカー品質でテストする場合、例えば「途中でネットワークケーブルが抜かれて通信が切れても大丈夫?」とか「途中でブラウザを終了してしまい、再度立ち上げてアクセスしたらどうなる?」とか「途中で機器の電源をOFFにして(ry」なんてことも想定して開発・テストするので、テストの自動化にも限界がある。どうしても手動でテストする部分が残ってしまう。

そこに組み合わせの爆発が重なるので、もう、何というか、手動テストの量が減らない。そんなこともあって、組み合わせを減らす為にメジャーなOS・ブラウザの組み合わせに絞り込む傾向にあると思う。

デバッガ

最近のブラウザは標準でGUIデバッガが付いているので、それを使う。Internet Explorerなら8以降にて開発者ツールでデバッガを使うことができる。ChromeSafariのデバッガはWebkitのものだからだろうか、似たような操作方法だ。

Firefoxにはデバッガは付いていないけど、大抵は定番のFirebugを入れるだけで事足りる。

悩ましいのはデバッガの使い方が異なる点だ。大別しても、

この3種類で操作方法が分かれる。なので短期間では中々それぞれのデバッガの使い方を深めることができない。

テスト

テストに関してはJavaScript界隈はC言語C++よりも先進的だ。というか単体テストに関して言えばJavaScriptは言語機能的に非常にテストし易い上にテスト・フレームワークも充実していて、Cプログラマからするととてもうらやましい非常にけしからん。

やり易い所で、まずは単体テストからテスト・フレームワークを導入してみるとよいだろう。組込み系だと「関数単位で単体テスト」という現場も多いと思うので、そこにテスト・フレームワークやテスト用モックを導入するのは難しくないはずだし、単体テストのレベルではブラウザを使わずDOMエミュレートで代用しても許されると思う*12

但しC言語でgcovを使うような感じのカバレッジ測定は諦めた方がよい。ScriptCoverとかJSCoverageとかあるけど、正直この手の機能は言語処理系のサポート無しで頑張るのは割に合わない。

ブラウザを使ったテストについても自動化の為の仕組みが幾つか出ている。私は時間とスキルの足らなさ故に検討すらできなかったのだけど、余裕があれば導入を検討した方がよいだろう。恐らく全項目を自動化することは無理だろうけど、半分でも自動化できれば浮いた時間をサポート対象ブラウザの拡大に使えるし、不具合修正後の回帰テストも楽になる。

ただ組込み機器でクライアントサイドJavaScriptでテストする場合、組込み特有の事情が生じるので通常のWeb開発のようにはいかない部分もある。

まず、デバッグ作業を含めて「修正して再テスト」のサイクルが遅くなる可能性が高い。これは、通常のWebアプリの場合、

  1. サーバ上のJavaScriptファイルを直接編集するか、又はローカルで編集したファイルに置き換える。
  2. テストを実行。

――のようなサイクルに近い感じになると思うのだけど、組込み機器の場合は、

  1. ローカルでファイルを編集。
  2. 修正したファイルを含むフォームウェア・イメージファイルを作成。
  3. テスト用の機器にファームイメージを書き込む。
  4. テストを実行。

こんな感じ。(2)、(3) の工程は頑張っても幾らか時間がかかるので、どう自動化しようとも微妙な待ち時間が発生してしまう。*13

また並行して異なるテストを実施したい場合、その分だけテスト用のハードウェアが必要となる可能性も高い。ブロードバンド・ルータの類のWeb管理画面を思い浮かべてもらえば分かるように「外部から機器を制御する」という目的の機能であることが多いので、そもそも複数同時接続ができない。また「機器の制御」という点でハードウェアとの密着度が高いし且つハードウェアが一般的なPCとは大きく異なるので、「仮想PC上に環境を構築して云々」という方法も難しい*14

そもそもシミュレータ等を使ってハードウェアとソフトウェアをうまい具合に分けることができたとしても、開発中盤ぐらいからは実機上でソフトを動かすはずだ。でなければハードウェアをチェックできないし、ハードとソフトの摺合せもできない。最終的に製品を動かすだろうお客様は、我々が開発したソフトウェアをシミュレータではなく実機上で動かすのだ。ならば本番同様に「ハード+ソフト」の組み合わせでテストしなくては意味がない。シミュレータと実機とで差異が無いとどうして言えようか?

更にテスト用にサーバ側に何かを仕込むのも難しい。何しろ「サーバ側 == テスト対象の組込み機器」なことも多い訳で、テスト対象に何かを仕組む(つまり出荷時の構成と異なる状態でテストする)こと自体が忌避される。仮にOKが出たとしても、今度は仕込むこと自体が手間だ。ファームの書き込み先に十分な空き容量があるか? 仕込むモノがバイナリなら依存するライブラリ等を含めて自分でビルドして、ファームイメージをビルドする工程にねじ込まなくてはならない(あとで原状回復することを考慮して)。HTTPサーバは当然Apacheではないけど大丈夫? あと組込みLinuxならともかくITRONやμITRONならどうする?

こういう問題はあるものの、JavaScript界隈のテストの仕組みをうまいこと導入できれば作業は楽になるはずだ。少なくとも単体テストに関しては(ソース行単位のカバレッジを諦めれば)CやC++よりも楽だと断言する。

*1:特にJavaScriptを知らない他の言語のプログラマ。腕に覚えがあるし見慣れたC言語系の文法なので、ろくにJavaScriptについて研究せずに突っ走っちゃったパターン。

*2:特に除算の結果に注意。あとビット演算のコストとか。

*3:Node.jsとかはちょっと事情が異なるらしいけど。C言語グローバル変数は本当にグローバルなのだけど、static変数があるお陰で「他のモジュールが抱える変数は(多分staticにしてあるから)見えないし、自分のモジュールの変数(もちろん基本はstatic変数)と衝突することはないだろう」と高を括っていても大抵は問題にならない――むしろ問題になるような開発現場ならC言語を使うべきではない。

*4:C言語グローバル変数の場合はリンクに失敗するので、アプリのビルド自体ができない。

*5:しかし正規表現自体に「ツールによって書き方や使える機能に差異がある」という罠が……。

*6:ブラウザごとにXMLHttpRequestレベルで挙動が異なる。それを上位のライブラリでどこまでカバーしているかが問題。

*7:ソースをコンパイルして、出てきたエラーや警告を潰してく作業。必ず警告も潰すこと。警告オプションを厳しめに設定するのがポイント。

*8:当然ながらこのあたりはブラウザ(というかそのブラウザに組み込まれているJavaScriptエンジン)によって異なる。

*9:まあ実際には「動的型付けのスクリプト言語」に限らず比較的モダン且つ流行の言語にはテスト・フレームワークが存在するし、そういった勢いのある言語のユーザにはTDDなどの技法の導入に熱心な人も多いように思う(偏見かもしれないけど)。逆にC言語のような枯れた言語の世界では、言語機能が足を引っ張る為にモダンな言語の環境でのようなテストがやり難かったりする。

*10:ある程度しっかりとテストを書いていればC/C++コンパイルチェックよりも広い範囲をチェックできるけど、テストがざるなら文法チェック程度にしかならないだろう。

*11:文字コードの件はミニファイ以外の各種ツールの選定にも影響してくる。

*12:もっとも私の場合、フレームワークとしてQUnit + QUnit-TAPを導入したものの、DOM周りは自前でスタブを書いた。なのでスタブを使う部分のテストはRhinoで実行した(それ以外はPhantomJSで実行。テスト実行時の処理系が2つに分かれた理由は歴史的経緯による)。

*13:もちろん回避方法はある。例えばファームイメージのデバッグ版をビルドする時にtelnetdとnfs-clientを組み込んで有効化しておけば、修正したファイルを置いてあるディレクトリをNFSマウントしてファイルのみ置き換えることができる。デバッグ時の修正確認では重宝する。ただ組込みLinuxではRAM上にファイルシステムを展開して動作していることが多く、この方法ではRAM上のファイルを書き換えているに過ぎないので、再起動すれば元に戻ってしまう。

*14:開発環境にもよるけど、シミュレータ付きの既製の基板を使うならともかく、自前で基板を起こす場合はまず間違いなく不可能に近い。