やじうまの杜
JavaScriptで「そうはならんやろ」「なっとるやろがい!」という事例が見つかる
割と古典的な罠らしい
2022年5月20日 10:00
「やじうまの杜」では、ニュース・レビューにこだわらない幅広い話題をお伝えします。
久しぶりに「JavaScript、なんもわからん……」となったネタを目にしたので、今回はそれを紹介しようと思います。
ど、どういうこと…#JavaScriptpic.twitter.com/P4S5kCljak
— lotz (@lotz84_)May 17, 2022
以下のコードは、配列におさめられた文字列型のデータを「parseInt()」関数を使って整数型に変換しようとしたものです。
['10', '10', '10'].map(parseInt)
→ [10, 10, 10] // 意図した結果
最近のJavaScriptに不案内な方のために説明すると、「map()」というメソッドは配列のそれぞれの要素に引数の処理(コールバック)を適用します。C#に慣れた方なら、LINQの「Select()」と同じといえばわかると思います。つまり、以下のような感じです。
['10', '10', '10'].map(parseInt)
// 以下の処理と等価(……のつもり)
[parseInt('10'), parseInt('10'), parseInt('10')]
しかし、このコードを実際に実行すると、以下の結果になります(「NaN」は「非数(Not-A-Number)」を表します)。
['10', '10', '10'].map(parseInt)
// 実際の結果 → [10, NaN, 2]
[parseInt('10'), parseInt('10'), parseInt('10')]
// 実際の結果 → [10, 10, 10]
これはどういうことなのでしょうか。
まず、「map()」は配列内の要素を対象にコールバックを実行しますが、実はその際、現在処理している配列のインデックス(添え字)と、配列そのものを渡そうとします。つまり、以下の処理を行います。
// array は['10', '10', '10']を指す
parseInt(array[0], 0, array)
parseInt(array[1], 1, array)
parseInt(array[2], 2, array)
また、「parseInt()」にも秘密があります。実は最大で2つの引数をとることができるのです。3つ目は無視されるので、前述の処理は以下のように解釈されます。
parseInt(array[0], 0) // parseInt('10', 0)
parseInt(array[1], 1) // parseInt('10', 1)
parseInt(array[2], 2) // parseInt('10', 2)
「parseInt()」の第2引数は「基数」(2進数、8進数、10進数みたいなヤツ)を指定するもので、「2」から「36」の範囲内で指定します。「1」など、無効な値を指定した場合は「NaN」を返します。基数が「0」の場合は少し複雑で、16進数(0xで始まる場合)か10進数と解釈されます。
つまり、それぞれのコールバックの結果は以下のようになります。
parseInt('10', 0) // → 10(10を10進数で解釈)
parseInt('10', 1) // → NaN(第2引数が無効なので「NaN」を返す)
parseInt('10', 2) // → 2(10を2進数で解釈)
['10', '10', '10'].map(parseInt)
// → [10, NaN, 2]
ちなみに、当初意図した通りに動作させるには、ちゃんと引数を1つであることを明示すればよいようです。基数を指定しておくとより安全でしょう。
['10', '10', '10'].map(str => parseInt(str))
// → [10, 10, 10]
['10', '10', '10'].map(str => parseInt(str, 10))
// → [10, 10, 10]
この「parseInt()」関数の一見意味不明な挙動は割と昔から知られているようで(参照:A JavaScript Optional Argument Hazard、JavaScriptのオプション引数の危険性)、MDNのドキュメントにもちゃんと書かれていました。これに「map()」のあまり知られていない仕様が組み合わされて、意図しない結果を生み出した……ということのようですね。
筆者は日頃、あまりJavaScriptを書かないので、とても勉強になりました。