echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

【JavaScript】数値に関する謎(?)挙動について

はじめに

前回まで、Git管理やバージョニングについて記事にしましたが、今回は一転してみんな大好き?JavaScriptの話です。

最近ちょっとSNSで話題になってた、JavaScriptの数値に関する謎?挙動についてです。

アジェンダ

  • parseInt()の挙動
  • 「+」「-」に関する挙動

parseInt()の挙動

parseInt(引数を数値に変換する関数)の挙動について、ある外国人のエンジニアの方が、こんな内容のツイートをしてました。(リンクは失念)

/*
 mysterious behavior of parseInt
*/
console.log(parseInt(0.5));  // 0
console.log(parseInt(0.05));  // 0
// (中略)
console.log(parseInt(0.000005));  // 0
console.log(parseInt(0.0000005));  // 5 ←What!?
   

上記に対して、Twitterでもいろいろツイートが飛び交ってましたね。まあ色々と。

上記の挙動について

まず、parseIntに数値を指定してるのがあまりよろしくないという点に注意です。(意味がない)

で、parseIntに数値(文字列以外)を指定した場合の挙動ですが、下記のようになります。(引数の数値をそのまま返却するわけではない点に注意)

  1. 引数をtoString()で文字列に変換する
  2. 上記文字列を数値にした値を返却する

参考:parseInt() - JavaScript | MDN

で、0.0000005をtoString()すると、下記の通り「5e-7」と指数表現された文字列が返ることが分かります。
f:id:Makky12:20220205192210p:plain

これを数値に変換するわけですが、文字列「5e-7」の「e」は数値(=10進数)に変換できないため、先頭の「5」のみ返された結果、結果が「5」となるわけです。

なお引数を文字列で指定すれば、0.0000005(やさらに小さい値)でも、正しい結果が返ります。
f:id:Makky12:20220205192828p:plain

ちなみにこの件については「1時間プログラミング」の紀平拓男さんもブログを書いてますので、そちらもぜひ。
JavaScript で parseInt / parseFloat を使わない方が良い理由

また、先述のMDNページでも下記のように「とても大きな数字やとても小さな数字を使用する際に予期しない結果を生み出すことがあります。」と記載してあります。

数値によっては e の文字を文字列表現の中で使用しますので (例えば 6.022E23 は 6.022 × 1023 を表します)、parseInt を使用して数値を切り捨てると、とても大きな数字やとても小さな数字を使用する際に予期しない結果を生み出すことがあります。parseInt を Math.floor() の代用として使うべきではありません。

基数変換でも

なお上記の文字列/数値の挙動の違いですが、8進数の基数変換の際にも起こります。
下図の通り、parseIntで8進数(っぽい値)の基数変換を行った場合、文字列or数値、基数(第二引数、以下「radix」と記載)の有無で値が変わります。

f:id:Makky12:20220206180908p:plain

まずややこしいのが、JavaScriptの8進数の扱いが文字列or数値で異なる点で、先述のMDNページの説明にもある通り、下記の挙動となります。

  • 文字列:先頭が0の値を8進数として扱わない
    • 先頭の0を除いた値を10進数文字列として扱う
  • 数値:先頭が0の値を8進数として扱う

上記により先頭2つのparseIntは、文字列「021」を文字列「21」として扱います。
そして、parseInt("21")はそのまま21、parseInt("21", 8);は「8進数の21=10進数の17」を返します。

また後ろ2つはまず数値021を10進数に変換→その値を文字列にする...が行われ、結果文字列「17」が返ります。
そして、parseInt("17")はそのまま17、parseInt("17", 8);は「8進数の17=10進数の15」を返します。

こんな感じで、parseIntを使った8進数の基数変換は結構ややこしい挙動をするので、扱いには要注意です。

16進数の場合は...

先述のMDNページの記載にもある通り、先頭が「0x(0X)」の文字列は8進数と違い、ちゃんと16進数として扱ってくれます。
なので下図の通り、parseInt("0x21")およびparseInt("0x21", 16);はどちらも33を返します。

ただ数値の場合は注意が必要で、まず0x21を10進数に変換し(=33)、それをradixで判定するので、3つめはそのまま33、最後のは「16進数の33(=0x33)=10進数の51」となり、51が返ります。

個人的には、parseIntについてはこんな感じかなあと思います。

  • 極力parseIntは使うべきではない。
    • 紀平さんのブログにもある通り、Numberを使うべき
    • 8進数の変換をする必要がある場合、ちゃんと挙動を理解して使うか、あるいは専用の関数を用意する
  • 第一引数は必ず文字列にする

f:id:Makky12:20220206182741p:plain

「+」「-」に関する挙動

また結構前ですが「+」や「-」の挙動について、別の外国人のエンジニアの方が、こんな内容のツイートをしてました。(これもリンクは失念)

/*
 mysterious behaviour of numeric-string operands
*/
console.log("2" + 2) // string "22"
console.log("22" + 2) // string "222"
console.log("222" - 2) // number 220 ←What!?
   

上記の挙動について

上記ツイートした外国人の人が言いたかったのは、

  • 「+」だと文字列なんだけど...
  • 「-」したとたん数値になってる!?

ってことなのかな?

まずJavaScriptの「+」には、

  • 文字列の場合:文字列の連結
  • 数値の場合:数値の加算

の2つの機能があります。

で、最初の2つは、左辺が文字列なので「文字列の連結」の方が実施されます。(左辺と右辺のいずれかが文字列なら、文字列連結が優先されるようです) f:id:Makky12:20220205195826p:plain

しかし「-」は「+」と違い、文字列を扱う機能はなく、単に「数値の減算」のみ行います。

最後の計算は左辺の文字列「222」が数値に変換可能な文字列なので「222 - 2」が実施され、結果220(数値)が返されます。(なお、数値に変換不可能な文字列だった場合、NaNになります)

まとめ

以上、最近SNSで話題になった、JavaScriptの謎?挙動に関する記事でした。

たしかにぱっと見「なんじゃこりゃ!?」というような挙動がJavaScriptには結構ありますが、そこでただディスるだけではなく、その仕組みを追っていくと「なるほど」とか「そういうことか」と勉強になる点も多々あります。

実際、parseInt()の挙動についてはかなり勉強になりました。(ただ、parseInt(021, 8) が15になる挙動は、理解するのに結構時間がかかりましたが...)

それでは、今回はこの辺で。