potisanのプログラミングメモ

プログラミング素人です。昔の自分を育ててくれたネット情報に少しでも貢献できるよう、情報を貯めていこうと思っています。Windows環境のC++やC#がメインです。

JavaScript 文字列とサロゲートペアの注意

2021/3/10:この記事は別のブログで投稿した記事を移動したものです。

まえがき

JavaScriptの文字列はUTF-16なのでサロゲートペアを含みます(少なくとも2019年12月6日のMDN バイナリー文字列)。例えば文字列"012abcあいう"では想定通り動作するコードも文字列"0aあ🍊"に対しては想定外に動作する場合があります。今回はその具体例として文字列にArray.from関数を適用した場合と解決策の一つを紹介します。

間違ったコードと注意点

文字列にArray.from関数を適用すると文字の配列が得られます。

Array.from("01aあ🍊")
// Array(5) [ "0", "1", "a", "あ", "🍊" ]

Array.from("01aあ🍊", s => typeof(s))
Array.from("01aあ🍊", s => s.length)
// Array(5) [ "string", "string", "string", "string", "string" ]
// Array(5) [ 1, 1, 1, 1, 2 ]

適用結果について次の2点に注意が必要です。

  1. 戻り値はstring型の配列であり、lengthを持つ。
  2. 文字["0"、"1"、"a"、"あ"]lengthは各1、文字"🍊"lengthは2。"🍊"はサロゲートペア。

この性質により次のコードは想定外の結果を返します。"🍊"(lengthは2)がs[0]により想定外の"\ud83c"へ変化します。

Array.from("01aあ🍊", s => s[0])
// Array(5) [ "0", "1", "a", "あ", "\ud83c" ]

Array.from("01aあ🍊", s => s[0]).join("")
"01aあ\ud83c"

正しいコード

文字列中のサロゲートペアに対応するにはss[0]だけの場合とs[1]もある場合の両方を考慮します。Array.from関数の第2引数mapFns.lengthの数だけ求める処理を繰り返してからflat関数でまとめることは一つの解決策です。

Array.from("0aあ🍊", s => [...Array(s.length).keys()].map(i => s[i])).flat()
// Array(5) [ "0", "a", "あ", "\ud83c", "\udf4a" ]

Array.from("0aあ🍊", s => [...Array(s.length).keys()].map(i => s[i])).flat().join("")
// "0aあ🍊"

Array.from("0aあ🍊", s => [...Array(s.length).keys()].map(i => s.charCodeAt(i))).flat()
// Array(5) [ 48, 97, 12354, 55356, 57162 ]

備考:文字列のlengthArray.from関数

文字列のlengthサロゲートペアを2文字として扱う長さを返します。Array.fromlengthサロゲートペアも1要素とする文字列配列を返します。したがって、これらの返す長さはサロゲートペアが含まれる場合に一致しません。

"01aあ🍊".length
// 6
Array.from("01aあ🍊").length
// 5(サロゲートペアが1文字扱いなのでlengthと異なる)