Chapter 02

一章 JavaScript、その危険な薫り

みりん書房
みりん書房
2023.03.26に更新

まずはJavaScriptの言語仕様の問題点について簡単に触れておきます。JavaScriptの言語仕様は必ずしも洗練されたものではありません。その背景には、そもそもJavaScriptは非常に急いで作られたプログラミング言語だという事実が存在します。

実際、JavaScriptの父・アイク氏は、慌てていたので幾つかの危険な仕様を仕込んでしまいました。ところで、JavaScriptは一体どれくらい短い期間で作られた言語なのでしょうか。この疑問について、dotJS 2017の「A Brief History Of JavaScript」と名を打った講演で彼は次のように語り始めました。

22年前の5月、私は10日間の激務をしました。碌に眠らないで働いて…

実は、JavaScriptはわずか10日間のうちに作られた言語なのです。アイク氏は以下のようにも述べています。

それは信じられないほどの急ぎの仕事でもあったので、ミスもありました。私が重要だと思うのは、間違いやギャップがあることを知っていたので、言語として非常に順応性があるようにしたことです。

では、このような急ぎの仕事によって、一体どんな危険な種が振り撒かれてしまったのでしょうか。ここでは、簡潔に3つ挙げてみます。

typeof null

試しに、次のようなコードを書いて結果をコンソールに出力してみます。その出力は、他の言語からJavaScriptに来た方にとっては目を見張るものかもしれません。しかし、ベテランのJSerにとっては、「今更何を言っているんだ、当たり前じゃないか」と思うようなことです。

console.log(typeof null);

ここでコンソールに出力されるのは、”null”ではなく”object”です。JavaScriptでは、nullのtypeはnullではなく、objectだというのです。これは一体全体どういうことなのだろうか、と考え出したくなります。実のところこれも「単に急いで作ったから」に過ぎないというのが結論のようです。アイク氏自身もこれがミスであることを認めています。

harmony:typeof_null [ES Wiki]

JavaScriptでは、メソッドを持たないデータ型でobjectでないものを指して、Primitiveと呼びます。しかし、JavaScriptにおけるnullはビルトインのデータ型であると同時にobjectもあります。このことによって、nullはPrimitiveなのか、それともPrimitiveではないのか、非常に混乱を来しています。一応、公式的な見解では「nullはPrimitiveではない」ということになっているようですが、やはり他の言語の感覚からすると、nullはPrimitiveであって当たり前の話なので、かなり気持ち悪い仕様ですね。

また、値がオブジェクト(一般に言うところの、nullを含まないオブジェクト)であるかどうか判断したいときには、

if(typeof value === "object" && value !=null){
}

と書かなければいけません。明らかにa!=nullが冗長な感じがしますが、悲しいことにこれはJavaScriptにとって必要な記述なのです。必要な記述なのですが、うっかりすると忘れてしまい、大きなバグにつながってしまいそうです。

今となってはこのような危険な仕様を修正することはもう困難なようで、JavaScriptの癒えぬ傷です。諦めて受け入れるしかなさそうです。「歴史に”もしも”はない」とはよく聞く言葉ですが、アイク氏があのとき十分な睡眠時間を取れていれば、などと考えたくなりますね。

いい加減なDate

JavaScriptの仕様について、Dateの仕様もときに大きな問題になりえます。すでに、アイク氏がJavaScriptをわずか10日で作らざるを得なかったことはお伝えしました。いくらアイク氏が非凡なプログラマーだったとはいえ、10日間でプログラミング言語を作るなど荒技です。当然の結果として、手を抜かざるを得なかったところもあるのでしょう、皺寄せは日付処理の実装に来ました。

激烈に短い納期に間に合わせるため、JavaScriptの日付処理の実装はJava(JDK1.0)のjava.util.Dateからコピーされました。今でこそJavaの日付処理は扱いやすいものになっていますが、当時のJavaのjava.util.Dateは相当、使いにくいものだったようです。その負の遺産が今もなおJavaScriptには残っています。この辺りの歴史的な経緯については、”Matt Johnson: The Past, Present, and Future of JavaScript Date and Time APIs”に詳しいので、ご興味のある方は覗いてみることをおすすめします。

では、具体的にどこがDateの悪い点なのでしょうか。まずは、以下の例です。

const date1 = new Date("2023-02-14")
console.log(date1) // -> Mon Feb 14 2022 09:00:00 GMT+0900 (日本標準時)

const date2 = new Date(2023, 1, 14)
console.log(date2) // -> Mon Feb 14 2022 00:00:00 GMT+0900 (日本標準時)

この例では、どちらも今年のバレンタインデーを示す日付をDateに渡しています。

最初に気になるのは、なぜ下のDateの日付が(2023, 2, 14)ではなく(2023, 1, 14)なのか、ということです。これは、JavaScriptのDateにおいては、月だけが0はじまりだからです。英語圏では”Feb 14 2022”のように、月だけは数値で表さないことが一般的であるため、月だけが0始まりに実装したものと思われます。しかし、他のプログラミング言語では月を1始まりのインデックスとして扱うのがより一般的です。(2023, 1, 14)という日付を見たら、きっと誰しも1月14日だと思うことでしょう。2月になるなんて、さすがに予想外すぎます。今更嘆いても仕方がありませんが、これは年も月も日も1始まりに統一してほしかったですね。

問題はそれだけではありません。上記の例では、同じ日付を指しているのに出力される時間が違うというも注目すべき点です。"2023-02-14"を渡すのと(2023, 1, 14)を渡すのとでは9時間の差があるのです。非常にややこしいですが、この時間差は「タイムゾーン文字列を含まない日付文字列をDateの引数に渡した場合はUTC として扱」うというDateの仕様によるものです。

しかし、これもまだまだ序の口です。どんどん行きます。以下をconsole.log()で出力したら、どんな値になるでしょうか。

new Date("0").toISOString()     // -> ?new Date(0).toISOString()       // -> ?new Date(0, 0).toISOString()    // -> ?new Date(0, 0, 0).toISOString() // -> ?new Date( 99, 0, 1).toISOString() // -> ?new Date(100, 0, 1).toISOString() // -> ?new Date( 99, 0, 1).getYear() // -> ?new Date(100, 0, 1).getYear() // -> ?const a = new Date(2004, 1, 29)
a.setFullYear(2003)
a.toISOString() // -> ?const b = new Date(2004, 2, 31)
b.setMonth(3)
b.toISOString() // -> ?

ちょっと立ち止まって、当てずっぽうでいいので上の10個のDateがそれぞれどんな出力になるのか想像してみてください。答えはきっと予想を裏切るものになります。では、結果を見てみます。

2000-01-01T06:00:00.000Z
② 1970-01-01T00:00:00.000Z
③ 1900-01-01T06:00:00.000Z
④ 1899-12-31T06:00:00.000Z

⑤  1999-01-01T06:00:00.000Z
⑥ 0100-01-01T06:00:00.000Z
⑦ 99-1800

⑨ 2003-03-01T03:00:00.000Z

⑩ 2004-05-01T03:00:00.000Z

「いや、なんでやねん!わかるかい!!」と思わずツッコミを入れたくなるような結果ですね。わかりにくいを通りこして、これはもはや人間が予測できるような出力の結果ではありません。とりわけ、⑨や⑩の日付の計算結果は意味不明です。⑧が負の値になるのも衝撃的ですが、誤植ではなく事実です。

ただ、先ほどのtypeof nullと違って、日付については、明るい将来の見通しもあります。煩雑なDateの仕様を乗り越えるべく、Temporalと呼ばれる新たら日付のAPIが既に提案されています。Temporalは、現在、仕様が完成した状態にあり、後は実装を待つのみというStage 3: Candidateの段階です。また、Temporalを待たずとも、day.jsdate.fnsといった日付処理のライブラリを使う手段もあります。

暗黙的な型変換

JavaScriptの良くない点の1つには、暗黙的な型の変換も挙げられます。これは、作った本人さえ後悔している仕様です。次のような例は、JavaScriptの暗黙的な型変換の例として有名なものですが、出力がどうなるのか正しく答えられるでしょうか。

1 + 2 + "1" // -> ?
②  1 + "2" + 1 // -> ?

答えは以下のようになります。

"31""121"

このような計算結果になるのは、JavaScriptが勝手に型の変換を行なっているからです。①では、先に1 + 2が計算され3になり、3+”1”が計算されるとき3が文字列の”3”に変換されるため、”3”と”1”の文字列の結合として、答えは”31”になります。②では、1 + "2"が行われるときに暗黙的な型変換が発生し、”1”と”2”を結合した文字列の”12”になった後、”12”と”1”が文字列結合して答えは”121”になります。

ここに挙げたのは、まだ比較的わかりやすい方の暗黙的な型変換です。実際には、もう少し複雑なケースもあり、思わぬ型変換が発生して罠に嵌ることもあります。複雑な型変換といえば、次のような面白い例もあります。wtfjsからの引用です。

 ('b' + 'a' + + 'a' + 'a’).toLowerCase() // -> ?
  (![]+[])[+[]]+(![]+[])[+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]] // -> ?

これは超難問で、JavaScriptの上級者でも正しい答えを導き出すのは困難かもしれません。console.log()で出力したら一体どんな結果になるでしょうか。結果は以下の通りです。

'banana''fail'

「正気か」と問いかけたくなるような出力ですね。どうしてこうなるのかは説明も難しいですし、紙面の都合上で割愛します。 少しだけ書くと、+ + 'a'は’a’が数値に変換できない文字列なので、NaNになり、あとは文字列結合で”baNaNa”、これを小文字にして”banana”です。2番目の例については…いやこれはもう考えるのも嫌です。ここで重要なのは、JavaScriptでは思いもよらぬ形で型変換が起こるというところです。足を掬われてしまいそうな、なんとも危険な仕様ですね。

一章のまとめ

本章では、JavaScriptの特徴、JavaScriptの光と影の部分について簡単に触れました。JavaScriptの光の部分は、世界中で最も広く使われているプログラミング言語の1つであり、日々進化し続けている言語だということです。反対に、JavaScriptの影の部分は、柔軟すぎる言語であるという点です。加えて、標準の言語仕様にもいくつかの問題があることを述べました。

JavaScriptについて悪く書きすぎてしまったのではないかと心配ですが、上記に挙げた欠点は、JavaScriptの扱い方を心得ているとある程度、対処できます。これから、危険を孕んだJavaScriptというプログラミング言語をどのように扱っていけば、読みやすく、セキュアに書けるかということを考えていきます。

では、次の第二章でリーダブルなJavaScriptについて具体的に見ていきましょう。