2006-04-02

近況

Shibuya.js のイベント に申しこんだ. が, メールアドレスを間違えたらしく登録確認のメールが来ない. 再申しこみをしようとしたら満員御礼. がっくり. JavaScript なんて嫌いだ. 今日は JavaScript の悪口を書こう.

JavaScript の暗黒面を覗く

"Ajax IN ACTION" を読んで以来 AJAX 界隈を信じきれずにいる. ただ私も他人をとやかく言えるほど JavaScript のことをよく知らない. Bookmarklet を書いたり仕事のデモを作る程度. 文法の知識もいいかげんで, 型なし Java のサブセットのように使っていた.

そこで不信感を晴らすべく少し JavaScript を勉強してみることにした. Web アプリケーションで仕事をしている友達に教えを乞うと, 仕様書がいちばんわかりやすいとのこと: "ECMAScript Language Specification (3rd Edition)". 読んでみるとたしかに詳しい. (あたりまえ.) そしてやはり知らないことは多い. せっかくなので以下にそれを並べてみた.

先に "悪口" と言ったのは, いまいちな感じの新発見が多かったから. 私と同じように考える人はそれなりにいるらしく, 次バージョンの JavaScript にそれらの機能を変更するよう求める記事もあった: "JavaScript 2.0: Evolving a Language for Evolving Systems(PDF), Waldemar Horwat". これもあわせてちょっとだけ紹介.

セミコロンの自動挿入

この仕様には驚いた.

var x = hello()

var x = hello();

として解釈される. ";" が増えているのに注目.

基本的には, ";" がなくて文法エラーがあったとき, ";" を入れることで文法違反が解消されるなら ";" を勝手に補ってくれるというもの. たぶん onclick のようなイベントハンドラ用属性にインラインでスクリプトを 書くための機能だろう.

<div onclick="hello()">←これを OK にしたい.</div>

困るのは仕様がアグレッシブすぎること. 仕様書からの例をひくと

return
a + b

return;
a + b;

になったりする. 勢いあまってバグになりそうだ. たとえばうっかり改行を入力してしまうだけで戻り値が undefined になってしまう. インラインスクリプトを認めるという目的のためなら プログラムの最後にだけセミコロンを補完すれば十分. これはやりすぎだと思う.

Waldemar は "strict mode" を提案しており, このモードでは semicolon insertion はかなり制限されることになっている.

値型とオブジェクト型

ECMAScript では, 整数や文字列のような基本的なデータ型の表現が 値型とオブジェクト型で二通りある. Java にプリミティブ型の int とオブジェクトの Integer があるようなもの.

log("val:" + typeof(1.0)); // val:number
log("obj:" + typeof(new Number(1.0))); // obj:object

なんだかまぎらわしい. どちらか一方でいいとおもう. Waldemar も primitive type はやめて object に統一しろと言っている.

Java にプリミティブ型があるのはもっぱら性能上の理由からだが, ECMAScript で速度を気にしても仕方ない. それに, Ruby のように数値型のオブジェクトを効率よく扱うことはできる.

Java ではプリミティブ型があってもただ面倒なだけだった. 型付けが動的で型変換も多い JavaScript だとより厄介なことがおこる. 次に示す等価演算子はその典型例.

二種類の等価比較

JavaScript には等価演算子がふたつある. ふつうの比較は "=" ふたつ, もうひとつの比較である厳密な比較は "=" みっつ.

まず普通の比較:

log(1 == 1); // true

型変換もしてくれる:

log(1 == new Number(1)); // true

型変換をしてくれるのが嬉しいとは限らない. たとえば:

log("1" == 1); // true

また, undefined 周辺は謎が多い:

log(undefined == null); // true
log(0 == null); // false

逆に厳密な比較は型変換をしない:

log(1 === new Number(1)); // false

保持する値が一緒でもオブジェクトが違っていてはダメ:

log((new String("Hello")) === (new String("Hello"))); // false

でも値同士の比較は OK.

log("Hello" === "Hello"); // true

Java でいう Object#equals() と 比較演算子 の違いに似ている気もするけれど, 型変換があるので混乱する. がんばって一本化してほしい.

配列

JavaScript の配列が hash であるというのは聞いたことがあったけれど, 試してみると思っていたより違和感があった.

まず普通に使ってみる.

var arr = new Array();
arr[0] = "hello";
log(propertyNameList(arr).join()); // "0"

添字の値が大きいと配列は自動的に拡張される, ように見える.

arr[10] = "hello";

しかし配列の実体は hash なので, 単に hash の要素が一つ増えるだけ.

log(propertyNameList(arr).join()); // "0,10"

とはいえ配列らしく振る舞うために length は適当な値に更新される.

log(arr.length); // 11

ここからが妙なところ. まず末尾の要素を削除.

arr.shift();
log(arr.length); // 10

正しい気もする. ただここで思いだして欲しいのは, 配列が単なる hash だということ. この配列は実際のところ arr[0] の要素しか持っていない. それなのに arr.length は 10. ちょっときもちわるい...

続けて, 実体が hash なら, といたずらをしてみたら不思議なことがおこる.

arr.baz = "Baz";
log(propertyNameList(arr).join()); // "0,1,2,3,4,5,6,7,8,9,baz"
log(arr.length); // 10

これは一体どう説明すれば...と思ったものの, firefox と ie では挙動が違った. 標準仕様というわけではないらしい.

添字の型変換にもたじろぐ.

arr[2] = "bye";
log(arr["2"]); // "bye"

添字が文字列になっている点に注目. 逆も可能.

便利な気もする型変換だが, やはり期待通りに振る舞ってくれない.

var arr2 = new Array();
arr2[1.5] = "hoge";
log(arr2.length); // 0
log(propertyNameList(arr2)); // "1.5"

整数に丸めてくれればいいのに...

つづけて Number オブジェトを添字にしてみる.

var arr3 = new Array();
arr3[(new Number(2.5))] = "hoge";
log(arr3.length); // 0
log(propertyNameList(arr3)); // "2.5"
arr3[(new Number(5))] = "fuga";
log(arr3.length); // 6
log(propertyNameList(arr3)); // "2.5,5"

値が整数なら配列の要素として扱われ, 端数があると "hash の" 要素にはなっても "配列の" 要素にはならない. 浮動小数点の誤差で困りそう.

中途半端な Prototype

JavaScript のオブジェクトは Prototype という隠しプロパティを持っている. これはプロトタイプ指向の継承を実現するためにある, というようなことが 仕様書には書いてあるのだが, それが何なのか私はよく知らなかった.

そこでもう一つのプロトタイプ指向言語である Self のチュートリアルや記事を眺めてみた. Self と JavaScript のオブジェクトモデルはよく似ている. どちらも hash みたいなオブジェクトとメッセージの移譲連鎖がある. Self のオブジェクトには "parent" というスロット(プロパティ)があり, オブジェクト自身に存在しないスロット名の検索は parent に移譲される. parent スロットは JavaScript の Prototype 隠しプロパティに相当する.

JavaScript のイディオムでは Prototype プロパティをクラスオブジェクト(関数ポインタテーブル)のように使う. しかし Self では親クラスで定義されたプロパティを保持するのに使っている. メソッドを定義するには traits(mixin) という別の仕組みを使う.

JavaScript には traits がない. これはプロトタイプ指向の継承をするには道具が足りていないとも言える. そこで無理矢理に継承を実現するのは, C 言語でオブジェクト指向を実現するのに似ている. できなくはないけれどあまりやりたくない. この気分は gtk+ を使ったことのある人ならわかってもらえると思う.

(こんな話をしたら, そもそもオブジェクト指向では実装の継承はよくないものであって 無理に継承をする必要はない. 移譲を使うのが望ましいのだと 件の Web アプリプログラマに諭された. そうかも.)

ちなみに Waldemar はおとなしく class を導入しろという. それはそれでつまらない気もする...

with 文

var obj = new Object();
obj.foo = "Foo";
with(obj) {
  log(foo); // "Foo"
}

使い道がよくわからない...

ホスト環境で with 文をフックできるなら, Java の synchronized や C# の using のように 排他制御などスコープ単位の操作を実現できるのかもしれない. ただ今のところそういう機能はなさそう.

何かいい使い道を知っていたら教えてください.

Function オブジェクトの引数

Function オブジェクトのコンストラクタは自身が定義する関数の引数名を指定する. そのアイデアに不満はないけれど...

var baz = new Function("a,b", "c", "log('baz(' + [a,b,c].join() + ')');");
log(baz.length); // 3
baz(1,2,3); // "baz(1,2,3)"

これはエラーでいいと思う.

ラベル文

知らなかった機能の中で唯一気に入った機能がラベル文.

  var n = 10;
label_n:
  while (0 < n) {
    var m = 10;
label_m:
    while (0 < m) {
      log("m:" + m); // 一度だけ "m: 10" とプリント
      m--;
      break label_n;
    }
    log("n:" + n); // プリントされない
    n--;
  }

一種の goto として使える. 外側にしかジャンプしないから goto より安全だ. JavaScript は catch が例外の型を指定できないため throw が使いづらい. 局所的なエラーからの脱出はラベルと break を使う方がいいかもしれない.

Writing Solid JavaScript (が欲しい)

こうして見ると一見シンプルな JavaScript にもけっこう落とし穴がある. エラーにして欲しいところで何なく動いてしまったり, ふだんは正しくうごくけど時々変になったり, そういう機能がいくつかある. あまり安全な言語ではない. モジュール化のための仕組みもないから, 大規模コードが破綻する危険も大きい. 慎重に使わないとあぶない言語だと思う.

それでもウェブブラウザではもっぱら JavaScript しか動かない以上 避けて通るのは難しい. それに, 忌み嫌うほどひどい言語といわけでもない.

だから, 派手な UI を作るのも楽しいけれど, それとは別に JavaScript の持つプログラミング言語としての難しさを明らかにして, 堅牢で生産性の高い JavaScript プログラミングのためのプラクティスを 作る人がいていいと思う. 勢いだけで使うには落とし穴が多過ぎる. 私はまだ堅牢な JavaScript コードを想像できない. JavaScript 版の "Writing Solid Code" や "Effective C++" が欲しい. そいういう本があったら紹介してください.

ちぐはぐさとウェブらしさ

個人的には, JavaScript は変な言語だと思う. 仕様書を読んでいるとなんというかちぐはぐなかんじ. 勢いで実装してしまって, 標準化するというので あたふたとフォーマルな体裁を整えたのではなかろうか. ただ, 言語は変でもウェブページに動的な性格を与えるという アイデアには革新性があったのは明らかだ. Netscape の全盛期に開発された技術の風格がある.

ここからはまったくの想像なのだけれど, JavaScript を作った当時, 開発者は一刻も早くそれを製品(ブラウザ)に搭載するよう迫られていたのだと思う. ベンチャー企業は絶え間無い技術革新で顧客や投資家を繋ぎとめる必要があるからだ.

そうした圧力の下で言語処理系を洗練させるのが難しいのは想像がつく. とりあえず動くところまで辿りつくのが最優先. デモの翌月にはもうリリースで, その後も波のように打ち寄せる機能追加と不具合修正の嵐. 処理系本体の修正もままならない. そのうえ上司やマーケティングの人々は, 文法エラーを許せ, このサイトの(おかしな)コードを動くようにしろと無茶を迫ってくる. そうして "とりあえず動く" コードは "無茶な要求を乗り切る" ための ハックを重ね, 現在の JavaScript 辿りついた...のだとしたら, この妙な仕様は不可避なものなのかもしれない. そう思うと少しは共感もめばえてくる.

結局動くものが正しい のだから, 市場の判断を無視してケチをつけても仕方ない. むしろこのでっちあげハックなかんじも含めて JavaScript を楽しむ方が 今時の若者っぽくてよさそうな気もする. ぼちぼち使っていきたい. (という発言が既に年寄りじみてるけど...)

追記

よく見ると Shibuya.js からの確認メールは届いてました. よかった.