JSの論理演算子と三項演算子で事故った


結論から言えば演算子いっぱい使った式書くなら()つけとけって話なんですが、仕事でReact書いてる時に論理演算子と三項演算子を何も考えずに使っていて挙動が意味不明になったので調べてみました。

問題になったコード

bool1 && bool2 ? "trueコンポーネント" : "falseコンポーネント"
このコードの期待する結果としてbool1がtrueの時だけ表示、かつbool2でコンポーネントを出し分ける的なものを想定しており、そのために論理演算子と三項演算子つかって書いていました。
まぁ何も考えずに書いていたので、案の定レビューの時、分かりづらみというコメントをいただきました。🙇‍♂️
今思うと確かにクソわかりにくいコードを書いてますし、このコードだとbool1がfalseだと本来はどちらのコンポーネントも表示したくないのですが"falseコンポーネント"を表示してしまうので意図しないバグを産んでたのでレビューで救われました笑(適当に書くのはよくないよ⭐️)

この場合、自分が意図した挙動で表示するなら
bool1 && (bool2 ? "trueコンポーネント" : "falseコンポーネント")
とすればbool1がfalseの場合は表示しない様にできてbool2で表示するコンポーネントを分岐できます。

それか

bool1 && (
 <>
    {bool2 ? (
       "trueコンポーネント"
    ) : (
       "falseコンポーネント"
    )
  </>
)

って感じで分けた方がまだわかりやすいかもです。
とりあえず括弧つけとけばなんとかなります。
他にもやり方はあると思いますが、今回の話的には括弧付ければオッケーです。


話は終わりましたが、
あれ?括弧ついてない時ってどう評価されるのかな?ってちょっと興味が沸いたのでみていきます。

考えてみる

とりあえず問題となったコードを考えていきたいと思います。
しかし正直論理演算子と三項演算子どちらが先に評価されるのかわからんので、やっぱり考えるのをやめて全部の場合を書いて試してみます。

結果は

1.bool1 bool2共にtrue
→"trueコンポーネント"

2.bool1 true, bool2 false
→"falseコンポーネント"

3.bool1 false, bool2 true
→"falseコンポーネント"

4.bool1 bool2共にfalse
→"falseコンポーネント"

結果からみるにbool1がfalse、bool2がtrueでも"falseコンポーネント"が返ってくるということは、bool1で式が終わっておらず、bool2のtrueによって三項演算子が分岐していないので、
(bool1 && bool2) ? "trueコンポーネント" : "falseコンポーネント"
となってfalseコンポーネントが返されていると推測できます。

このことからあの状態のままだとbool1がfalseでもコンポーネントが表示されてしまい意図しない挙動となっていたと言えます。(怖いね👍)

ここら辺ちゃんと調べてみたいけどどうやって調べたら良いのかわからん。切に教えていただきたい。

論理演算子について余談

今回の修正で括弧をつけて
bool1 && (bool2 ? "trueコンポーネント" : "falseコンポーネント")
と書けば意図して動くやーんと思っていたのですが、よくよく考えてみると三項演算子で返ってきた結果は文字列だけどなんで結果として文字列を返すの??文字列返すって真偽値としてどうなってるの??意味不明😇

と挙動が謎だったので発狂しながら調べてみました。

とりあえずjs書いてみます。

実践卍

まずはシンプルに論理演算子の挙動をみたいのでbool型で試してみる。
true && false
→false
true && true
→true

ここら辺は想定内。
では文字列も含めてやってみます。

true && "false"
→"false"

true && ""
→""

false && "true"
→短絡評価でfalse

文字列を含めると文字列が返ってくる様になりました。
最後の部分に関しては論理演算子の短絡評価で最初がfalseなら後ろは評価しないやつ。その結果文字列trueは表示されずbool型のfalseが返ってきました。

bool型ではなかったらその値を返すみたいですね。。🤔
試しに数字でもやってみました
true && 1
→1
やはり真偽値でないものを返している様です。

式全体だとtrueと見なすのかな謎なので、if文に突っ込んでみました。

const isTrue = true && "";
if (isTrue) {
	console.log(true);
} else {
	console.log(false);
}

結果はfalse
と返ってきたので一応空文字""式的にはfalseの模様。

const isTrue = true && "trueになって欲しい";
if (isTrue) {
	console.log(true);
} else {
	console.log(false);
}

結果はtrue
文字列を入れるとtrueになった。

ということは空文字""は一応真偽値としてはfalseとみなされている。その結果
true && ""
これは式的にはfalseで、評価値は""ということになりました。
この場合論理演算子はbool型の評価値を返すのではなく、文字列を含む場合は評価値は文字列になるということなのか🤔

空文字はfalseとみなしていそうなので最初に持ってきてみる。
"" && false
→""
空文字なので式はfalse、評価値は""が返ってきて評価値が文字列なら文字列を返していそう。
ちなみに全部試すのめんどくさいですが、||だと
"" || false
→false
が返ってきます。
このことから、
最後に評価したものを評価値として返すが、それが真偽値以外ならそいつ自身を返す
と言えそうです。なので"" && falseで空文字""が返ってきたのは短絡評価で""の時点でfalseとみなされ後ろのfalseは評価されなかった結果、評価値を返す時にfalseではなく空文字の""が返されるのだと思います。

||を使うとさらにややこしくなりそうです。すでに理解が追いついていなくて調べたくないので、各自調べてもらえれば幸いミーです。

また下記の様にすると
"true" && "truetrue"
→"truetrue"
これは最初の部分に文字列"true"が入ってることでtrueとして扱われて、次に後の方が評価される。後ろの文字列"truetrue"はtrueなので式全体的にはtrue、そして最後が文字列なのでその文字列を評価値として返して"truetrue"となる。
やはり最後に評価したものを評価値として返していそうです。

そもそもJSちょっと特殊だった

JS以外の言語って論理演算子使う時真偽値しかとってくれなくて、文字列を真偽値として取ってくれない。自分はjsから勉強したから全部できるものだと思ってた。
全ての言語がそうかってわけではないと思うけどめんどいので調べてない。
phpとかだと文字列を取ってはくれるけど評価値は真偽値になってるっぽい。goとかだとそもそも真偽値同士でしか使えない。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Logical_Operators

&& および || 演算子は真偽値ではない値も使うことができるため、その場合は、真偽値ではない値を返すことがあります。

書いてあるやーんって感じ。

最後に

まぁとりあえず括弧つけようと思います。
こんなコード二度と書かないとここに誓います。✌️