やじうまの杜

JavaScriptで「そうはならんやろ」「なっとるやろがい!」という事例が見つかる

割と古典的な罠らしい

 「やじうまの杜」では、ニュース・レビューにこだわらない幅広い話題をお伝えします。

JavaScriptで「そうはならんやろ」「なっとるやろがい!」という事例が見つかる

 久しぶりに「JavaScript、なんもわからん……」となったネタを目にしたので、今回はそれを紹介しようと思います。

 以下のコードは、配列におさめられた文字列型のデータを「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を書かないので、とても勉強になりました。