potisanのプログラミングメモ

趣味のプログラマーがプログラミング関係で気になったことや調べたことをいつでも忘れられるようにメモするブログです。はてなブログ無料版なので記事の上の方はたぶん広告です。記事中にも広告挿入されるみたいです。

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と異なる)