「私、最近JavaScriptを書いているんですけど」
ある日の夕方、唐突に彼女が話しかけてきた。
「ふうん」
「今まではJavaやPythonを書くことが多かったんですけど、なんだかJavaScriptってバグを作りこみやすい気がするんですよね。今日も1行のバグを見つけるのに3時間くらいかかっちゃいました。」
いつの間にか彼女は僕の隣に座っていて、はあ、とため息をつきながら机に顔を伏せていた。
「それは大変だったね。どんなバグだったの?」
「直った今でも納得できないんですけど、if文の条件式がダメだったみたいなんですよ。0が入力されたときだけ処理したいところがあって、input==0って書いてたら」
「あ、空文字列が入力されて通っちゃってた?」
「そうなんですよ!はぁ、なんで分かっちゃうんですか。」
彼女は少し顔を上げて、こちらを見た。つり上がり気味の目が恨めしそうににらんでくる。
「だいたい、型が違うのに==で比較ができちゃう時点でおかしいんですよ。型エラーを吐いてその場で実行を止めて欲しいです。」
「まあ、動的型付けだしね。」
「でもPythonはちゃんと型エラーで止まるじゃないですか。」
確かにそうだ。動的型付けであっても、それは単に静的に型を付けていないだけであって、実行時には当然型があるので型エラーが検出できる。
「もう、適当なこと言わないでください。本当に大変だったんですから。それにしても、なんで'' == 0がtrueに評価されちゃうんだろう。百歩ゆずって型エラーじゃなくしたとしても、違うオブジェクトなんだからfalseになりそうじゃないですか?実際===はそういう挙動をしてくれるんですし……。」
「JavaScriptでは数値と文字列(それと真偽値)は特別扱いになっているからね。何が起きているのかを調べるために、ちょっと規格を見てみようか。」
そう言って僕は、本棚からECMA-262を取り出した。彼女も体を起こし、顔にかかった黒い髪を払ってからのぞき込んでくる。
「問題の箇所は11.9.3節だね。ちょっとルールが多く見えるけど、同じ型の組み合わせで左右が違うものも分けて書いていたりするだけで、実際にはたいしたことはない。とりあえず、今興味があるのは左辺がStringで右辺がNumberのときだから、ルール5を見てみよう。」
「If Type(x) is String and Type(y) is Number, return the result of the comparison ToNumber(x) == y
.って書いてありますね。ってことは、右辺の0はそのまんまで、左辺はToNumber('')に変換されるってことですよね。」
「その通り。問題は空文字列を数値に変換するとどうなるかということなんだけど、答えは9.3.1節を読むと書いてある。A StringNumericLiteral that is empty or contains only white space is converted to +0.
ってところだね。」
「つまり、最初の式は結局0 == 0になるから、これはルール1.c.iiiよりtrueですね。うーん、ルールをたどれば確かにそうなるけど、空文字列を数値に変換したら0になるのが納得いかないです。どんな数値でもないんだから、せめてnullとかNaNになってくれればいいのになぁ。」
本当に納得できないらしく、彼女は眉をひそめてむくれたような顔で文句を言っている。
「このへんはPerlから影響を受けているのかもしれないね。そういえば、納得できないついでにこんなのもあるよ。[] == ![]ってどうなると思う?」
「どうせ、例によってオブジェクトが真偽値に変換されるんですよね。でも、[]がTruthyだろうがFalsyだろうが!で真偽を反転したものが元のものと一致するとは思えないですし、falseじゃないんですか?」
「ところが、これはtrueになるんだよ。」
僕がそう言った瞬間、また適当なことを言ってからかおうとしているな、とでも言いたげな、彼女が時折見せる強気な目でこちらを見てくる。
「嘘じゃないよ。実際にnodeのREPLとかで試してもいいんだけど、規格を追ってみよう。まず、右辺を評価すると[] == falseになる *1。」
「そこまではいいです。」
「次に、もう一度さっきの11.9.3節を見てみる。今回はオブジェクトとBooleanの比較なのでルール7が適用されるから、まずfalseが数値に変換される。9.3節のTable12を見ると、これは0になるね。」
「なんだかC言語っぽい変換ですね。」
「さて、そうすると式は[] == 0となるから、今度はルール9が適用される。このルールによれば[]はToPrimitive([])で変換されるんだけど、9.1節と8.12.8節より、ざっくり言えば[].valueOf()と[].toString()のうち、先にプリミティブ型を返したものの返り値になる。」
「それで、ええと、[].valueOfは実際にはObject.prototype.valueOfで(15.4.4節)、このメソッドはレシーバそのものを返すから、結局[].toString()の返り値がToPrimitiveの結果になりそうですね。オチが見えてきました。」
「そう、その通り。Array.prototype.toStringは単にjoinを呼ぶだけで、Array.prototype.joinは長さが0の時には空文字列を返す。ここから先はさっき見た通りで……」
「空文字列は0に変換された上で比較されるから、結局[] == 0は0 == 0になるんですよね。ほんとだ、trueになっちゃった……。」
彼女は小さな口をぽかんと開けて、 信じられない、という表情で計算用紙を見つめていたが、ふと気付いたように一箇所を指さした。
「これって、ToPrimitiveの定義がちょっと怪しいというか、雑ですよね。Truthyなんだから直接真偽値に変換してToNumberすればいいのに、わざわざvalueOfとかtoStringとかを呼んで、遠回りしてから数値表現を得ようとしてます。」
「それは気付いてなかったけど、言われてみればそうだね。数値が欲しいのにtoStringなんかを呼んだところで、数値として意味のある値が返ってくるケースはほとんどなさそうだ。まあ、JavaScript的には、とにかくプリミティブ型が得られればいくらでも相互に変換できるし、エラーを出して止まるよりは適当に動いとけっていう感じなのかもね。」
「そんなのダメですー!」
机を叩き、立ち上がりながら叫ぶ彼女。気がつくと太陽はほとんど沈みかけている。かすかに残る西日が彼女の頬を薄く染め、腰まである長い髪を背景にして、くっきりとした輪郭を描き出していた。
*1:![]がどう評価されるかは省略