Help us understand the problem. What is going on with this article?

文字列を1文字ずつ配列化(サロゲートペアを考慮)

More than 1 year has passed since last update.

JavaScriptで𩸽(ホッケの漢字)🌕(満月の絵文字)を扱おうとすると、やや面倒な事になります。

例えば、文字列から「1文字ずつに分解した配列」を生成したい時。
普通はsplitメソッドを使用した以下のような処理でうまく生成できます。

コード
"満月の夜にホッケ食べたい".split("")
生成されるデータ
["", "", "", "", "", "", "", "", "", "", "", ""]

しかし、冒頭で挙げた文字を含んでしまうとうまくいきません。

コード
"🌕の夜に𩸽食べたい".split("")
生成されるデータ
["", "", "", "", "", "", "", "", "", "", ""]

絵文字や漢字の箇所が、1文字のはずなのに2つに別れて格納されてしまい、そのうえ見事に文字化けしてしまいます。
この記事では、これを解決する方法について紹介します。

原因

このセクションには、誤解を招くような説明が含まれている可能性があります。可能であれば、自分で調べてください。

これは、𩸽(ホッケの漢字)🌕(満月の絵文字)が「サロゲートペア」に該当する文字だから起きてしまう現象です。

JavaScriptの文字列は、UTF-16という形式でUnicodeの文字を扱うようになっています。
そして、UTF-16は、基本的には1文字を2バイトで扱います。

文字
Unicode U+6E80 U+6708 U+306B U+306F U+30DB U+30C3 U+30B1
UTF-16 U+6E80 U+6708 U+306B U+306F U+30DB U+30C3 U+30B1
データ 0x6E 0x80 0x67 0x08 0x30 0x6B 0x30 0x6F 0x30 0xDB 0x30 0xC3 0x30 0xB1

ところでUnicodeでは、文字はU+0000からU+10FFFFまで定義されています。しかし、UTF-16では1文字が2バイトなので、0からFFFFまでしか扱えません。1048576個足りなくなってしまいます。
そこで、U+10000からU+10FFFFの文字は4バイトで扱うように定められました。具体的には、「U+D800からU+DBFFの文字」を前半の2バイト、「U+DC00からU+DFFFの文字」を後半の2バイトとして組み合わせ、4バイトで扱うようにしています。
UTF-16では2バイトで1文字ですが、U+10000からU+10FFFFの文字は例外で1文字 = 4バイトで表現するようになっています。
これがサロゲートペアです。

文字 🌕 𩸽
Unicode U+1F315 U+306B U+306F U+29E3D
UTF-16 U+D83C U+DF15 U+306B U+306F U+D867 U+DE3D
データ 0xD8 0x3C 0xDF 0x15 0x30 0x6B 0x30 0x6F 0xD8 0x67 0xDE 0x3D

しかし、JavaScriptの文字列関係の関数やメソッドは、文字列を1文字 = 2バイトで扱うように作られています。
すなわち、1文字 = 4バイトのサロゲートペアに対応していません1
サロゲートペアを扱おうとすると、無理矢理2バイトずつに分割して処理してしまいます。
分割された2バイトのデータは、一応は「U+D800からU+DBFFの文字」または「U+DC00からU+DFFFの文字」です。しかしこの文字は、サロゲートペアのための特殊な文字として定められていて、普通の「表示するための文字」としては扱えません。
このため、文字化けしたような表示になってしまいます。

コード
"🌕には𩸽".split("")
生成されるデータ
["", "", "", "", "", ""]

前述した「1文字のはずなのに2つに別れて」しまうのも、「4バイトで1文字」のサロゲートペアを2バイトの1文字として分割してしまうせいです。

対応方法

スプレッド構文(スプレッド演算子)を使用した方法

ECMAScript 2015(ECMAScript 6)から追加されたスプレッド構文(スプレッド演算子)を使用すると、以下のように記述することで簡単に分解することができます。

コード
[..."🌕には𩸽"]
生成されるデータ
["🌕", "", "", "𩸽"]

ただし、この方法は古いWebブラウザでは使用できません。
最近のWebブラウザはほとんど対応しているものの、Internet Explorerでは使用できません。
実際に使用する時は、Babelなどで古いブラウザ向けに変換しましょう。

Array.fromメソッドを使用した方法

ECMAScript 2015(ECMAScript 6)から追加されたArray.fromメソッドを使用すると、以下のようなコードで分解することができます。

コード
Array.from("🌕には𩸽")
生成されるデータ
["🌕", "", "", "𩸽"]

この方法も、古いWebブラウザでは使用できません。
実際に使用する時は、Babelなどで古いブラウザ向けに変換しましょう。

なお、古いブラウザ向けにArray.fromメソッドだけを追加定義してくれるPolyfill(ポリフィル)という分類のライブラリもありますが、だいたいのものは文字列のサロゲートペアを考慮していないため、この用途では使えません。
MDNのページに載っている「ポリフィル」も対応していません。
このため、Polyfill(ポリフィル)の使用はオススメしません

どうしてもPolyfill(ポリフィル)を使用したい場合は「mathiasbynens/Array.from」を使いましょう。コレはサロゲートペアに対応しています。

matchメソッドを使用した方法

JavaScriptでのサロゲートペア文字列のメモ - Qiita #IV-II. サロゲートペアに対応した配列化

この記事にあるように、正規表現でサロゲートペアを考慮した文字を取得できるようにすれば解決します。
例えば以下のようにすると、文字列を正しく分割してくれます。

コード
"🌕には𩸽".match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\s\S]/g)
生成されるデータ
["🌕", "", "", "𩸽"]

空文字列の場合に生じる問題の解決方法

ただし、この方法は、空文字列が指定された場合にnullが返されてしまう問題があります。

コード
"".match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\s\S]/g)
生成されるデータ
null

そこで、||演算子をエルビス演算子のように使用してこう書きます。

コード
"".match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\s\S]/g) || []
生成されるデータ
[]

または、空文字列の場合にもマッチするよう正規表現を修正し、filterメソッドで空文字列を除外します。

コード
"".match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\s\S]|^$/g).filter(Boolean)
生成されるデータ
[]

これで、常に配列が返ってくるようになります。


  1. ただし、ECMAScript 2015(ECMAScript 6)から追加された関数やメソッドはサロゲートペアに対応しています。例えばString.fromCodePoint関数やString.prototype.codePointAtメソッドがこれに該当します。 

sounisi5011
最近はNode.jsでTypeScript製のライブラリ開発ばかりして遊んでる無職(大学院生)です。古のPHPや、HTML5、CSS3などの知識もあります。 正規表現もそれなりに扱えますが、JavaScriptとPHPで学んでいるので、アマチュアレベルの実力だと思っています。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした