Chapter 03

二章 JavaScriptと条件分岐

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

プログラミングをしていて、もっとも頭を抱えることといえば、複雑に入り組んだ条件分岐ではないでしょうか。出口のないifの迷路に入り込み、何時間も彷徨い続けるのは非常に辛い体験です。そうならないために、本章ではどのように書けばJavaScriptでスッキリ条件分岐できるのか考えていきます。

リーダブルな条件分岐の基本

まず、一般に言われている読みやすい条件分岐を書くための技術のうちの多くは、そのままJavaScriptにも適用可能です。次の例を見てみます。

if(20 <= age){
    return "ADULT"
}

一見、普通のコードのように思えて、「おや、何か気持ち悪いぞ」と思われた方もいるかもしれません。それは、この条件分岐が一種の倒置法だからです。日本語で考えてみても「20歳より年齢が上なら、あなたは成人だということになります」などと気持ち悪い言い方はしません。「年齢が20歳より上なら、あなたは成人だということになります」と表現するのが一般的でしょう。同じように、プログラミングの条件分岐も、主語や調査する対象を先に持ってくると明快です。比較する値は、調査する対象の後に置くようにします。これはif文に限らず、次のようにfilterfindの中で行う比較についても、同様です。

// ⭕️調査する対象を先、値を後にする
const adults = members.filter(member => member.age >= 20);
const cto = members.find(member => member.position === "CTO");

// ❌調査する対象を後、値を先にすると読みづらい
const adults = members.filter(member => 20 <= member.age);
const cto = members.find(member => "CTO" === member.position); 

早期リターンのすゝめ

「早期リターン」という手法も条件分岐を見通しよく書くために積極的に取り入れたいテクニックです。早期リターンとは、簡単に言えばreturnを使って早めにif文を抜けて条件分岐のネストを減らすことです。この早期リターンはJavaScriptを書くときにも非常に有用なものなので、頭の片隅に入れておくとリーダブルなフロントエンドが書けると思います。例を見てみます。

const submitTweet = (text) => {
  if (text !== null) {
      if (text !== "") {
          if (text.length <= 240) {
              return "TWEETED!"
          } else {
              throw new Error("Invalid value of text: text exceed max length")
          }
      } else {
	        throw new Error("Invalid value of text: text is empty"
      }
  } else {
    throw new Error("Invalid value of text: text is null")
  }
}

上記の処理は単純に、ツイートの内容が妥当なものであれば”TWEETED!”の文字列を返し、そうでない場合は適当なErrorを投げるというものです。至極シンプルな関数なのに、なんとなく読みづらいですね。次のように変えたらどうでしょうか。

const submitTweet = (text) => {
  if (text === null) {
	  throw new Error("Invalid value of text: text is null")
  }

  if(text === ""){
    throw new Error("Invalid value of text: text is empty")
  }

  if(text.length >= 240){
    throw new Error("Invalid value of text: text exceed max length")
  }

  return "TWEETED!"
}

こうすると、どんなときにどんなエラーが吐かれるのかぱっと見でわかるようになりました。先ほどの例よりも、エラーを吐く条件と、吐かれるエラーの内容が近くに書かれており、その対応関係が把握しやすくなっています。

早期リターンとは、上記のようにthrowやreturnを使って、ifのネストを減らすテクニックです。早期リターンの中でも、この例のようにthrowで例外を投げて関数を抜けるパターンを「ガード節」と呼ぶこともあります。MartinFowlerのFailFastという記事は、早期リターンについて理解するのに、非常に役立ちます。

Jim Shore and Martin Fowler created the concept of Fail Fast in 2004. This concept is the basis for the “return early” rule. While failing fast, the code is more robust because of the initial focus in finding the conditions where the code execution can terminate
. With this approach, bugs are easier to find and fix.

ところで、本書は早期リターンを推奨する立場ですが、一部には「早期リターンは禁止すべきだ」という意見もあるようです。その論拠としては、関数の出口が1つであった方がコードをデバックしやすいということが言われています。しかし、近年はIDEのデバッガーも優秀になりましたし、こうした古い慣習を気にする必要はありません。名著と名高いリーダブルコードでも次のように書かれています。

if文の順番関数で複数のreturn文を使ってはいけないと思っている人がいる。アホくさ。関数から早く早く返すのは良いことだ。

ということで、早期リターンを毛嫌いしている方も、ぜひ一度、導入を検討してみてはいかがでしょうか。

find, every, some, includes…メソッドを使いこなそう

JavaScriptには、配列を操作するための強力なメソッドが数多く用意されています。これらをうまく使いこなすことで、冗長だった条件節もすっきり書くことができます。今回ご紹介するそんな便利なメソッドはfind, every, some, includesの4種類です。この4つのメソッドがどのようにif文を見通しよくしてくれるのか、さっそく見ていきましょう。

例として、次のようなクラスメイトの配列を用意しました。

const classmatesOf2He = ["関内", "風浦", "大草", "大浦", "音無", "加賀", "木津", "木村", "小節", "小森", "常月", "根津", "日塔", "藤吉", "丸井", "丸内", "三珠", "糸色"]

ある日、風浦の母親は、娘が今日の授業で使う体操着を家に忘れてことに気づき、教室まで届けに行こうとしました。しかし、肝心の娘の所属している教室がどこなのかが分かりません。確か、2のへ組だったと思うのですが、合っているでしょうか。それを確かめるには、次のような方法があるかもしれません。

if(classmates[0] === "風浦" || classmates[1] === "風浦" || classmates[2] === "風浦" ...省略...) {
    return "娘の体操着を届ける"
}

しかし、教室には24人の生徒がいるので、24個もの条件を書いて||でつなぐのは少々面倒です。そこで、includesが使えます。includesは、対象となる配列が、引数として受け取った要素に一致するものを含んでいた場合のみtrueを返します。よって、次のようにすることができます。

if(classmatesOf2He.includes("風浦")) {
    return "娘の体操着を届ける"
}

これなら、条件を簡単に書くことができますね。また、もし仮に、クラスメイトの名簿に、苗字だけでなく名前の記載もあるとしたらどうでしょう。次のような名簿です。

const classmatesOf2He = ["関内 マリア", "風浦 可符香", "大草 麻菜実", "大浦 可奈子", "音無 芽留", "加賀 愛", "木津 千里", "木村 カエレ", "小節 あびる", "小森 霧", "常月 まとい", "根津 美子", "日塔 奈美", "藤吉 晴美", "丸井 円", "丸内 翔子", "三珠 真夜", "糸色 倫"]

母親が娘の名前をちゃんと覚えている場合には、先ほどと同じように、

if(classmatesOf2He.includes("風浦")) {
    return "娘の体操着を届ける"
}

と書くことができます。しかし、娘の名前をうっかり忘れてしまった場合には、どのようにすれば良いでしょうか。「どないなってなってんねん、おかん。娘の名前忘れてもうて〜。」というようなケースです。その場合には、somefindを使うことが可能です。

someは、配列の中に条件に合致するものが一つでもあればtrueを返すメソッドです。また、findは、配列の中で最初に見つかった条件に合致するものを返す返すメソッドです。これらを使って次のように、体操着を届けるべきクラスかどうかを確認できます。

// ①someを使う例
if(classmatesOf2He.some(classmate => classmate.split(" ")[0] === "風浦")) {
    return "娘の体操着を届ける"
}

// ②findを使う例
if(classmatesOf2He.find(classmate => classmate.split(" ")[0] === "風浦")) {
    return "娘の体操着を届ける"
}

ここで、findを使った例に、少し違和感を感じる方ももしかするといらっしゃるかもしれません。つまりclassmatesOf2He.find(classmate => classmate.split(" ")[0] === "風浦"かの返す値は、”風浦 可符香”なので、if()の括弧の中に、Booleanではなく”風浦 可符香”という文字列が渡されていることになります。これは、大丈夫なのでしょうか。

結論からいうと、これはJavaScriptでは正しい書き方です。JavaScriptは、if文の()の中に文字列以外の真偽値以外を渡しても、正常に処理を行うことができます。何がtrueと判断される値で、何がfalseと判断される値なのかは、追って説明します。この場合、”風浦 可符香”はtrueと判断される値です。そのため、②はちゃんと "娘の体操着を届ける"をreturnしてくれます。

さて、すでにお気づきの方もいらっしゃるかもしれませんが、上記の例では、漫画『さよなら絶望先生』からクラスメイトの名前をお借りしています。この漫画は、何かしら性格に難のある24人の絶望少女達によって紡がれる物語です。つまり、ここで出てきた2のへ組はクラスメイト全員が絶望少女です。(実際には男子生徒もいますが、ここでは話を単純化させてください)。逆にいうと、クラスメイト全員が絶望少女であればそのクラスは2のへ組ということになるでしょう。最後に、そのことを検証してみましょう。

const classmatesOf2He = [
  { name: '関内 マリア', isDespairGirl: true },
  { name: '大浦 可奈子', isDespairGirl: true },
  { name: '大草 麻菜実', isDespairGirl: true },
  { name: '音無 芽留', isDespairGirl: true },
  { name: '加賀 愛', isDespairGirl: true },
  { name: '木津 千里', isDespairGirl: true },
  { name: '木村 カエレ', isDespairGirl: true },
  { name: '小節 あびる', isDespairGirl: true },
  { name: '小森 霧', isDespairGirl: true },
  { name: '常月 まとい', isDespairGirl: true },
  { name: '根津 美子', isDespairGirl: true },
  { name: '日塔 奈美', isDespairGirl: true },
  { name: '藤吉 晴美', isDespairGirl: true },
  { name: '丸井 円', isDespairGirl: true },
  { name: '三珠 真夜', isDespairGirl: true },
  { name: '糸色 倫', isDespairGirl: true },
];

クラスメイト全員が、絶望少女(despairGirl)であればそのクラスは2のへ組です。先ほどと同じように、ここでも、

if(classmates[0].isDesapirGirl && classmates[1].isDesapirGirl && classmates[2].isDesapirGirl && ...省略...) {
    return "2のへ組"
}

と書いてしまうのは、冗長に過ぎます。ここでは、everyを使うと綺麗に書くことができます。everyは、配列の中の要素がすべて条件に合致するものがすべてあればtrueを返すメソッドです。「すべてに合致がevery、1つでも合致がsome」というように、everyとsomeはセットで覚えておくと良いでしょう。次のように書くことができます。

if(classmatesOf2He.every(classmate => classmate.isDespairGirl)) {
    return "2のへ組"
}

この項では、配列操作のメソッド(find, every, some, includes)を条件分岐の判定に用いる方法をご紹介しました。JavaScriptの配列のメソッドは種類が多く、覚えるのが難儀だと感じるかもしれないですが、ぜひ多様なメソッドを使いこなせるようになってください。そうすると、条件分岐も綺麗に書くことができます。

オブジェクトを使った条件分岐

また、JavaScriptにおける条件分岐のコツには、単純な対応関係を示す場合には、if文やswitch文ではなくオブジェクトを使うという手段があります。以下は、APIから返ってくるエラーコードに対応して、特定の文字列を返す例です。

const getErrorMessage = (errorCode) => {
    switch (errorCode) {
    case "EMAIL_ALREADY_EXISTS":
      return "同じメールアドレスを持つユーザーがすでに登録されています。"
    case "USER_NOT_FOUND":
      return "指定されたユーザーは見つかりません。"
    case "INVALID_CONFIRM_CODE":
      return "確認コードが間違っています。"
    case "INVALID_PASSWORD":
      return "パスワードが間違っています。。"
    case "INVALID_PASSWORD_LIMIT_EXCEEDED":
      return "パスワードが間違っています。一定時間以上おいて、再度お試しください。"
    default:
      return "不明なエラーが発生しました。"
  }
}

const errorMessage = getErrorMessage("EMAIL_ALREADY_EXISTS")

とりわけ読みにくいということはないですが、この条件分岐はerrorCodeとerrorMessageの単純な対応関係を示すものであるため、if文やswitch文を使わなくともシンプルにオブジェクトで表現することが可能です。次のようにするのはどうでしょうか。

const getErrorMessage = (errorCode) => {
  const errorMap = {
    EMAIL_ALREADY_EXISTS: "同じメールアドレスを持つユーザーがすでに登録されています。",
    USER_NOT_FOUND: "指定されたユーザーは見つかりません。",
    INVALID_CONFIRM_CODE: "確認コードが間違っています。",
    INVALID_PASSWORD: "パスワードが間違っています。",
    INVALID_PASSWORD_LIMIT_EXCEEDED: "パスワードが間違っています。一定時間以上おいて、再度お試しください。",
  }

  return errorMap[errorCode] ?? "不明なエラーが発生しました。"
}

const errorMessage = getErrorMessage["EMAIL_ALREADY_EXISTS"]

先ほどの例よりも、errorCodeとerrorMessageの対応関係がわかりやすくなったように思われます。このような条件分岐をシンプルにオブジェクトで表現する方法は、意外と汎用性の高いパターンです。単純な対応関係をswitch文で表現しようとしているときは、objectで書けないかどうか一考してみてください。

??と||の違い

先ほどのコード例でerrorMap[errorCode] ?? "不明なエラーが発生しました。"とあり、奇妙な??が見られました。これは、Nullish coalescing operator(null合体演算子)と呼ばれるもので、左辺が null undefinedの場合には右の値を返し、それ以外の場合に左の値を返す論理演算子です。EcmaScript2020で追加された比較的新しい仕様ですが、かなり便利な代物で、モダンJavaScriptを語る上でNullish coalescing operatorの存在は欠かせません。

しかし、新しい論理演算子??が登場する以前から、&&||という2つの論理演算子がすでに存在していました。なぜまた別の演算子が必要だったのでしょう。次の例でその理由を見ていきます。

const bourbon = "メーカーズマーク"
const whiskey = "ジャックダニエル"

const todaysLiquor1 = bourbon && whiskey // -> whiskey
const todaysLiquor2 = bourbon || whiskey // -> bourbon
const todaysLiquor3 = bourbon ?? whiskey // -> bourbon

todaysLiquor1は、バーボンとウィスキーが両方あったらウィスキーを飲もう、というコードです。対して、todaysLiquor2では、バーボンとウィスキーが両方あったらバーボンを飲もう、というコードです。なんだかアル中みたいな例ですが、この場合だとtodaysLiquor2todaysLiquor3の値は一緒ですね。次の例はどうでしょうか。

const bourbonStock = 0
const whiskey = "ジャックダニエル"

const todaysLiquor1 = bourbonStock && whiskey // 0
const todaysLiquor2 = bourbonStock || whiskey // "メーカーズマーク"
const todaysLiquor3 = bourbonStock ?? whiskey // 0

これ例だと、todaysLiquor2todaysLiquor3の値は異なり、||と?? の違いがはっきり見て取れます。

||は左辺がFalseと評価できる値であるときに右辺を返すのに対し、??は左辺が null undefinedであるときに右辺を返します。0はFalseと評価できる値ですが、もちろん、null undefinedとは異なる値です。そこで、todaysLiquor2が右辺を返すのに対し、todaysLiquor3が左辺を返すという結果になったいるということです。

以上に見てきたように、??||は使いどころが似ているように思えて、実際はまったく異なるものです。この違いを意識しておかないと、思わぬところで足を掬われてしまうかもしれません。

この節の冒頭に出てきた例を再掲しますが、errorMap[errorCode] ?? "不明なエラーが発生しました。"errorMap[errorCode] || "不明なエラーが発生しました。"も、一見、どっちを使っても良いように思えるかもしれません。

しかし、そうとも限りません。軽率な開発者が、次のようなエラーメッセージを追加してしまった場合を考えてみます。

const getErrorMessage = (errorCode) => {
  const errorMap = {
    EMAIL_ALREADY_EXISTS: "同じメールアドレスを持つユーザーがすでに登録されています。",
    USER_NOT_FOUND: "指定されたユーザーは見つかりません。",
    INVALID_CONFIRM_CODE: "確認コードが間違っています。",
    INVALID_PASSWORD: "パスワードが間違っています。",
    INVALID_PASSWORD_LIMIT_EXCEEDED: "パスワードが間違っています。一定時間以上おいて、再度お試しください。",
		// new
		SHOULD_IGNORE_ERROR: ""
  }

  return errorMap[errorCode] || "不明なエラーが発生しました。"
}

これは、getErrorMessage(”SHOULD_IGNORE_ERROR”)としたときに空文字を返すことを意図したコードですが、期待どおりにはなりません。空文字はFalseと評価できる値なので、getErrorMessage(”SHOULD_IGNORE_ERROR”)は、 "不明なエラーが発生しました。"を返します。

このようなことを考えると、Falsyの罠に陥らないために、積極的に??を使っていくのが良さそうです。

演算子 · JavaScript Primer #jsprimer

💡補足: falsyとtruthy

先ほどから、「falseと評価できる値」という言葉をたびたび用いていますが、JavaScriptではこれを”falsy”と呼びます。反対に「trueと評価できる値」を”truthy”と呼びます。

Falsyなは以下の通りです。

  • false
  • 0
  • -0
  • 0n
  • “”
  • null
  • undefined
  • NaN

truthyな値は、これ以外の値のすべてです。つまり、a14{}Infinitynew Date()truthyです。

JavaScriptを書いていると、falsyな値をfalseにしたり、truthyなtrueにしたりしたいことがあります。trueなのか、falseなのか、値をはっきり白黒つけたいときです。そうした場合は、Booleanコンストラクタを使うことでbooleanに変換することができます。

const isDisabled = true
const isDisabledOrUndefined = isDisabled || undefinedBoolean(isDisabledOrUndefined) // true

これは、!!isDisabledOrUndefinedのように!!を使っても意味は同じです。どちらも内部でtoBoolean()というメソッドを呼んでいるので、結果は常に等しくなります。どちらを使うか悩ましいところではありますが、私はBooleanを使っています。!!は単純に短くて便利ですが、JavaScript独特の記法であり、Booleanを使った方が他のメンバーにとって読みやすいかもしれない、という考えからです。

このように色々な書き方があってややこしいJavaScriptですが、次の章ではそれらの複数の書き方を比較しながら検討していきます。その前に、二章の話をまとめます。

二章のまとめ

本章では、JavaScriptの条件分岐をスッキリ読みやすく書くためのTipsをご紹介しました。読みやすい条件分岐の基本は、JavaScriptも他の言語も同じで、調査対象を左辺に持ってくること、また、早期リターンを用いる手法もあります。加えて、単純な対応関係を表す場合には、ifやswitchを使わないで、オブジェクトで簡潔に表現でき、これも有用なパターンです。本章の最後には??と||の違いについても触れましたが、これはしっかりと書き分けて行きたいですね。