三章 1つのことをするための無限の方法|[🚧編集中]リーダブルフロントエンド〜①JavaScript編〜

您所在的位置:网站首页 バンジー宣言 三章 1つのことをするための無限の方法|[🚧編集中]リーダブルフロントエンド〜①JavaScript編〜

三章 1つのことをするための無限の方法|[🚧編集中]リーダブルフロントエンド〜①JavaScript編〜

2023-03-25 00:44| 来源: 网络整理| 查看: 265

本書の冒頭で「JavaScriptは柔軟すぎる言語だ」と書きました。実際、JavaScriptには1つのことをするために、複数の方法が用意されています。色々な方法があると選択肢が増えて幸せになれるようにも思われますが、複数の選択肢のうちの何を選べばいいのか迷ってしまう場合もあります。そこで、この章では、それらの混乱を来たすような似て非なるものたちを比較して、どの方法を選ぶと良いのかを検討していきます。

数値判定

「複数の選択肢があってややこしい」という問題について語るとき、まず筆頭に上がってくるのは数値関連の機能です。数値関連の機能には、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オブジェクトの数値関連のメソッドとは異なり、まともに動作します。

isNaN(true) // -> true isNaN(false) // -> true 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を使わないで、オブジェクトで簡潔に表現でき、これは有用なパターンです。本章の最後には??と||の違いについても触れましたが、これはしっかりと書き分けて行きたいですね。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3