本書の冒頭で「JavaScriptは柔軟すぎる言語だ」と書きました。実際、JavaScriptには1つのことをするために、複数の方法が用意されています。色々な方法があると選択肢が増えて幸せになれるようにも思われますが、複数の選択肢のうちの何を選べばいいのか迷ってしまう場合もあります。そこで、この章では、それらの混乱を来たすような似て非なるものたちを比較して、どの方法を選ぶと良いのかを検討していきます。
数値判定
数値関連の機能は、JavaScriptの使用のうちでとりわけ複数の選択肢があってややこしい部分です。数値関連の機能には、GlobalオブジェクトのメソッドとNumberオブジェクトのメソッドの2種類が用意されており、初学者の方は混同するかもしれません。例えば、ある値がNaNかどうかを判別したいとき、isNaN(value)とする方法と、Number.isNaN(value)とする方法があります。
これらが同じものであれば、「どっち使ってもOK」という結論になり話は早いのですが、そうはいきません。実は、双方はまったく異なるものなのです。GlobalオブジェクトのisNaNは、以下のようにびっくりする結果になります。
isNaN(true) -> false
isNaN(false) -> false
isNaN(null) -> false
true
もfalse
もnull
も数値ではない値であり、NaN(Not a Number)
だと考えられます。したがって、本来なら、上記の演算はすべてtrueになるべきものです。しかし、GlobalオブジェクトのisNaNによる判定はfalseになっています。true
もfalse
もnull
も数値だといっているのです。これは、明らかに間違っています。
JavaScriptについては、このような「まじか…!」と思うような仕様が数多く残っています。なぜなら、JavaScriptはブラウザで動く言語であるという手前、明らかにバグと思われるような仕様でも後方互換性を捨てた修正を施すことができません。GlobalオブジェクトのisNaNの、trueが数字だと言い張るような頭のおかしい挙動も直されることがありません。
しかし、代わりにNumberオブジェクトが用意されています。Numberオブジェクトの数値関連のメソッドは、Globalオブジェクトの数値関連のメソッドとは異なり、まともに動作します。
Number.isNaN(true) // -> true
Number.isNaN(false) // -> true
Number.isNaN(null) // -> true
したがって、数値関連の機能は、Numberオブジェクトの方を使うようにしましょう。ただし、parseIntとNumber.parseInt、parseFloatとNumber.parseFloatに限っては、どっちを使っても機能的に違いはありません。
このような罠に陥らないためには、しっかりとESLint
を使っておくことが大切です。ESLint
はこのような危険なコードを判別して、我々にアドバイスをくれます。「そのコードやめときな!危ないよ!」と警告してくれます。口うるさいように思えて、実はとても優しい先生なのです。isNaNについても、もしESLint
が入っていれば「Number.isNaNの方を使ったほうがいいよー」とちゃんと教えてくれます。
JavaScriptには、あまりに罠がたくさんあるので、Lintは「使ったほうがいいもの」ではなく「必ず使わなくてはいけないもの」なのです。ESLint
なしでJavaScriptを書くなんて、ロープをつけずにバンジージャンプするようなもので、ただの自殺行為です。このことについては、第七章でより詳しく見ていきます。
やや話がそれてしまいましたが、結論としては、parseIntとparseFloat以外の数値関連の機能は、Numberオブジェクトのメッソドを使うようにしましょう。
parseIntとNumber
JavaScriptで文字列を数値に変換するのにも、いくつかの異なった方法があります。主にみられるのは、parseIntとNumber、それから暗黙的な型変換を使った方法です。次の例で、順に説明します。
const strNum = "999"
const num1 = Number(strNum)
const num2 = parseInt(strNum, 10)
const num3 = +strNum
これらすべての値、num1, num2, num3はconsole.log
で出力すると、どれも同じ結果になります。出力されるのは、数値型の999です。では、これら似たようなメソッドたちは、一体どれを使ったら良いのでしょうか。違いがわかるように比較検討していきます。
// ①boolean
parseInt(true, 10) // -> NaN
Number(true) // -> 1
+true // -> 1
parseInt(false, 10) // -> NaN
Number(false) // -> 0
+false // -> 0
// ②null
parseInt(null, 10) // -> NaN
Number(null) // -> 0
+null // -> 0
// ③空文字
parseInt("", 10) // -> NaN
Number("") // -> 0
+"" // -> 0
これらの比較結果からはわかるのは、まずNumber
とUnaryPlusOperator(+)
を使った暗黙的な型変換の結果には、ほとんど違いが見られないということです。BigInt
を変換するときだけは、Number
とUnaryPlusOperator
の変換結果に差異が生じます。しかし、parseInt
とNumber
の違いと比べると、微々たる違いと言えそうです。これらの理由により、parseInt
とNumber
の違いに絞って具に見ていきます。
Number
がbool値やnullや空文字を勝手に数字に変換しているのに対し、parseInt
はbool値やnullや空文字を渡すと、NaN
になっています。これは、Number
の方が良しなに変換してくれてありがたいという見方もできます。しかし、場合によってはありがた迷惑になります。stringを渡しているつもりが、うっかりbool値やnullを渡してしまっていたというケースでは、バグの発見が遅れてしまうかもしれません。
一方で、parseInt
にも問題がないわけではありません。次のコードを見てみます。
const num1 = parseInt('3a9b0c3d2e9f8g', 10); // -> 3
const num2 = Number('3a9b0c3d2e9f8g'); // -> NaN
数字とアルファベットが混合した文字列を渡したとき、parseInt
が最初に出てきた数値のブロックを返すのに対し、Number
はNaN
になります。これは「”1”や”999”のようなそのまま数値にできる文字列を渡していたつもりが、うっかり混合文字列を渡してしまった」というような場合に、ミスに気づきやすいように、Number
のようにNaN
を吐いてくれた方がありがたいような気がします。
ということで、bool値やnullや空文字の扱いについてはparseInt
に、混合文字列の扱いについてはNumber
に軍杯が上りそうです。両者の良いとこどりをする方法もあります。次のような実装はどうでしょうか。
const toInt = (strNum, radix = 10) => {
return typeof Number(strNum) === "number" ? parseInt(strNum, radix) : NaN;
}
toInt(true) // -> NaN
toInt(false) // -> NaN
toInt(null) // -> NaN
toInt("") // -> NaN
toInt("3a9b0c3d2e9f8g") // -> NaN
個人的には、これが理想の変換処理ですが、数値の変換でここまで込み入ったことをする必要もないかもしれません。どちらを使おうか迷ったら、とりあえずparseIntを使いましょう。
nullとundefined
少し話は変わりますが、null
とundefined
も、しばしばどちらを使うか議論の種になるところです。これらの二つの値の違いはよくトイレットペーパーホルダーに例えられます。nullとはトイレットペッパーホルダーにトイレットロールがセットされていない状態で、undefinedとはそもそもトイレットペーパーホルダー自体がない状態だというのです。なかなかわかりやすい比喩ですね。つまり、null
は「存在しない」ことを意味し、undefined
は「定義されていない」ことを意味します。
このようにいうと、null
とundefined
の間には、厳密な区別があるようにも感じますが、実際に開発していると、その意味の区別はなかなか難しくなります。そして、null
を積極的に使うべきだという人もいれば、undefined
を積極的に使うべきだという人もいます。我々は何を信じればいいのでしょうか。それぞれの主張を詳しく見ていきます。
まず、null派の考えでは、null
を積極的に使うべき理由は、null
が意図的に何もないものであることを示せるからです。null
は自然発生しませんが、一方、undefined
の方は自然発生します。次のコードはundefined
が自然発生することの例です。
let value;
console.log(value);
const obj = {};
console.log(obj.foo);
const arr = [];
console.log(arr[0]);
const func = () => {}
console.log(func());
この出力結果は、すべてundefined
です。このようにundefined
は、const value = undefined
と明示的に宣言しなくても、何らかの処理の過程で、うっかり紛れ込んでしまうかもしれない性質を持っています。これに対してnull
はプログラマーが意図的に使わない限り発生しません。つまり、null
を使うことで、何らかのミスでその値が存在していないのではなくて、値を「敢えて」存在させないことにしたのだとわかります。これは、一理ある考え方です。
次にundefined
派の考えです。このundefined
派の過激派の1人には”JavaScript: The Good Pattern”を書いたDouglas Crockfordという人物がいます。彼によれば、null
とundefined
は本質的に同じものなので、それらを使い分ける必要はありません。どちらか1つを使うべきであるならば、明示的に宣言しないと存在しえないnull
よりも、自然発生するundefined
を使う方が都合が良いです。また、アイク氏も「迷ったらundefined
を使おう」という趣旨の発言をされています。加えて、TypeScriptのコーディングガイドにもundefined
を使うようにという記述があり、この事実もundefined派の論拠として挙げられることが多いようです。
これらの両方を適切に使い分けようとする人もいます。しかし、2種類を使い分けるとなると、それだけ手間も増えます。コードレビューで「ここはundefined
を使うべきだ」とか「この場合はnull
の方がしっくりくる」という議論がたびたび持ち上がるようなら、開発はうまく進まず、難儀なことになるでしょう。そもそも、使い分けが難しいということは、すなわち、その2つのものに決定的な違いはないということなのかもしれません。一生懸命に議論してまで区別することには、その労力に見合う価値はありません。
つまり、undefined
とnull
を使い分けるメリットは大したものではないというのが結論です。 私自身はアイク氏に倣って、迷ったら常にundefined
を使うようにしています。しかし、プルリクエストのレビューで「ここはundefined
じゃなきゃ駄目だ」などと指摘することはないですし、この違いはこだわるべきところではないというスタンスでいます。
named exportとdefault export
今ままで、目的を果たすための数多くの方法があることを書いてきましたが、それはモジュール管理にも及びます。JavaScriptには、実に界隈にもさまざまなモジュールの仕様・フォーマットがあります。その中で特にはCommonJS Modules(cjs)、Umd Modules(umd)、ES Mdoules(esm)が挙げられます。このうち、ES Modulesはブラウザが直接的に理解できるモジュールシステムであり、ES2015で策定された、標準的なモジュールシステムです。そのため、フロントエンドの開発では、もっぱらこのESMが使われることが多いようです。
さて、このESMですが、次のようなモジュールシステムになっています。
export exampleFn = () => { console.log("example") };
このようにして、使いたいファイルをエクスポートすることができます。
また、これを呼び出す側のファイルでは、
import { exampleFn } from "./exampleFn
などのようにインポートすることができます。このように名前をつけた個別の関数やオブジェクトをexportする方法をnamed-export
と言います。
一方、これには別の方法もあります。
export dafault exampleFn = () => { console.log("example") };
と”default”をつけてエクスポートすると、
import exampleFn from "./exampleFn
のように呼び出すこともできます。この方法はdefault-export
と言います。
どちらの方法を使っても、ちゃんとエクスポートして、インポートできます。しかし、1つのプロジェクトで2つの方法が混在すると、少々面倒なことになります。できれば、プロジェクトの初期段階でどちらの方法を使うか、認識を合わせておくのが良いと思います。
本書で推奨するのはnamed-export
の方であり、default-export
は避けるべきだという意見です。default-export
の危険は命名をリファクタリングしたときに生じます。例えば、
export exampleFn = () => {
...中略...
};
という何の変哲もない関数があったとします。ところが、開発の途中で、この関数の内部に非常に重大なバグが潜んでいることが発覚しました。これは危ないと気づいたある開発者は、この関数を次のように命名しなおして、ほっと一安心、額に浮かんだ冷や汗を拭いました。新しい名前はこうです。
export default veryDangerousFnNoOneShouldUseThis = () => {
...中略...
};
もちろん、実際にこんな命名にするかどうかはおいといて、これは仮の話です。ですが、default-export
の場合、ファイルのパスさえ同じなら、たとえ関数名と一致しなくても好きな名前で呼び出せてしまいます。そのため、新しくveryDangerousFnNoOneShouldUseThis
という名前を冠することになったこの関数は、呼び出す側で古いimport exampleFn from “../exampleFn”
となったまま残り続けてしまうことがあります。
そのようなことがあると、せっかく変数名で注意を喚起してみても無駄に終わります。呼び出し側では羊の皮を被ったキツネ、つまりexampleFn
の皮を被ったveryDangerousFnNoOneShouldUseThis
が平気で使い回されてしまうかもしれません。
このように、好きな名前でimportできてしまうというdefault-export
の特徴は、開発における大きな落とし穴になりえます。このようなことを考えると、exportする側で名前が変わったら、importする側の名前も同じように変えなくてはならないnamed-export
は、多少の面倒はあるものの、安全性が高く、優れているといえるのではないでしょうか。
(コラム)自転車置き場の議論
上記のように、フロントエンドのプロジェクトをスタートさせるにあたって、よく議論になる諸問題について簡単に説明してきました。しかし、この章の初めにも書きましたが、あまり本気にしないでください。瑣末な問題とまでは言いませんが、これらは比較的重要度の低い問題です。
ところで、「パーキンソンの凡俗法則」という言葉を聞いたことがあるでしょうか。この法則は、又の名を「自転車置き場の議論」といます。一般によく起こる法則なのですが、特にソフトウェア業界では有名な法則です。ポール・ヘニング・カンプというソフトウェア開発者によって、世に知らしめられました。これは、「会議のそれぞれの議題に費やされる時間は、その議題の結論によってどのくらいのお金が動くかということと反比例する」という法則で、より端的にいうと「組織は些細な物事に対して、不釣り合いなほど重点を置く」という意味です。そんなはずはないと思うような法則ですが、現実にも、IT業界にも、頻繁に起こっている問題です。上記に挙げたフロントエンドでよく議題に上がるような「どっち使うか問題」も、ともするとパーキンソンの凡俗法則によって議論が過度に膨らんでしまうかもしれません。パーキンソンが語るのは次のような次のようなストーリーです。
その部屋に座っているのは、大きなタスクをアサインされた選りすぐりのメンバーでした。政府は、新しく建設する原子力発電所の可能な限り素晴らしい設計にするために、このメンバーを招集しました。
この会議の席には、核分裂と放射線の専門家、輸送とロジスティクスの第一人者、人員配置ディレクター、セキュリティキャプテン、財務責任者、オペレーション マネージャー、処分と廃棄物など、国内で最も優れた頭脳が集まっています。皆が皆、高い専門性のある人々ですが、すべてのことに関して詳しいわけではありません。それぞれの分野の専門家であっても、他の分野の専門家ではない場合があります。
会議の主要なアジェンダは、「新しいプラントにどのような技術を組み込むことができるか」をということです。核分裂と放射線の専門家は、「テクノドロイド制御と新しいチタン合金保持セルが、未使用のウランを保管する最も効率的な方法である」と発言しました。この分野の専門家以外の人間は、テクノドロイド制御やチタン合金保持セルが一体何なのかわかりません。そこで、とりあえず無言で頷いておくことにします。
次に、人員配置ディレクターは、「このプラントの予測出力を考えると、このプラントを効率的に運営するには、3 つのシフトのそれぞれに 50 人が必要である」と述べました。組織運営についての専門家ではない人は、自分の狭量な知見で口を挟んで恥をかきたくありません。ここでも黙って同意します。
各専門家が自分の専門分野とそれを実装する最善の方法について意見を述べ、メンバーは 各専門家の推奨事項に同意します。議論は一見すると、円滑に進んでいるように思われました。
ところが、社員生活コーディネーターが「建設する原子力発電所に、社員のための自転車置き場を併設することは可能ですか?」と聞いたとき、会議の状況は一変しました。テクノドロイド制御について知らない人も、効率的な組織運営と人員配置について知見がない人も、自転車置き場のことならよくわかります。このことについては誰もが専門家であり、誰もが意見を持っているのです。そのため、最も些細な問題であるはずの、自転車置き場の議題が、その会議では最も白熱した話題となってしまいました……。
長々とパーキンソンの凡俗法則について説明してしまいましたが、これは頭の片隅に置いておくべき法則かと思います。確かに、ソフトウェアの品質にこだわることは重要です。しかし、細部にまでこだわって議論しつくた結果、開発が一向に進まないようでは本末転倒です。
そこで本書の薦める方法としては、パーキンソンの凡俗法則に陥らないように、ある程度まで意見を交換し、結論が一致を見ないときは、「えいや!」で結論を下してしまってください。それはもう決めの問題なのです。そして、できるだけ本質的な議論により多くの時間を割くようにしましょう。なにせIT業界は慢性的に人手不足で、我々に与えられた時間は有限です。細かい議論にいつまでも拘泥している時間はありません。
三章のまとめ
本章では、「1つのことをするための無限の方法」という名を打って、人によってよく書き方が分かれる部分について説明していきました。特に、JavaScriptに
加えて、単純な対応関係を表す場合には、ifやswitchを使わないで、オブジェクトで簡潔に表現でき、これは有用なパターンです。本章の最後には??と||の違いについても触れましたが、これはしっかりと書き分けて行きたいですね。