vs. ジャバスクリプト
「私、最近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:![]がどう評価されるかは省略