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 |
データ(UTF-16BE) | 0x6E 0x80 |
0x67 0x08 |
0x30 0x6B |
0x30 0x6F |
0x30 0xDB |
0x30 0xC3 |
0x30 0xB1 |
データ(UTF-16LE) | 0x80 0x6E |
0x08 0x67 |
0x6B 0x30 |
0x6F 0x30 |
0xDB 0x30 |
0xC3 0x30 |
0xB1 0x30 |
ところで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 |
データ(UTF-16BE) | 0xD8 0x3C 0xDF 0x15 |
0x30 0x6B |
0x30 0x6F |
0xD8 0x67 0xDE 0x3D |
||
データ(UTF-16LE) | 0x3C 0xD8 0x15 0xDF |
0x6B 0x30 |
0x6F 0x30 |
0x67 0xD8 0x3D 0xDE |
しかし、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)からは、反復処理プロトコルというものが追加されています。これは、「Symbol.iterator
変数に入っているシンボル型の値」のプロパティを持つ値の事です。このプロパティにはメソッドが割り当てられており、このメソッドを呼ぶことで返ってくる反復子オブジェクトを使うことで、配列のような「複数の値を格納したオブジェクト」から値を1つづつ取り出せます。ECMAScript 2015以降は、配列も文字列も反復処理プロトコルに対応しています。
そして重要な事は、文字列の反復処理プロトコルを使うことで、サロゲートペアを考慮した文字の取り出しが可能になるのです。例えば、以下のようなコードを記述することで、サロゲートペアを考慮した「1文字ずつに分解した配列」を得ることができます。
function getCharArray(str) {
const resultArray = [];
const iterator = str[Symbol.iterator]();
while (true) {
const iteratorResult = iterator.next();
if (iteratorResult.done) break;
const char = iteratorResult.value;
resultArray.push(char);
}
return resultArray;
}
getCharArray("🌕には𩸽")
["🌕", "に", "は", "𩸽"]
しかし、こんな長いコードを書く必要はありません。ECMAScript 2015には、反復処理プロトコルに対応したオブジェクトを配列に変換するための構文や関数が存在します。
スプレッド構文(スプレッド演算子)を使用した方法
ECMAScript 2015(ECMAScript 6)から追加されたスプレッド構文(スプレッド演算子)を使用することで、上述した「反復処理プロトコルに対応するオブジェクト」を配列に変換することができます。つまり、以下のように記述することで文字列を簡単に分解することができます。
[..."🌕には𩸽"]
["🌕", "に", "は", "𩸽"]
ただし、この方法は古いWebブラウザでは使用できません。
最近のWebブラウザはほとんど対応しているものの、Internet Explorerでは使用できません。
実際に使用する時は、Babelなどで古いブラウザ向けに変換しましょう。
今日のWebブラウザは、全てスプレッド構文に対応しています。唯一非対応だったInternet Explorerのサポートも2022年6月15日に終了し、2023年2月14日以降は開くことすらできなくなります。そのため、気兼ねなく使えるでしょう。
Array.from関数を使用した方法
ECMAScript 2015(ECMAScript 6)から追加されたArray.from関数も、上述した「反復処理プロトコルに対応するオブジェクト」を配列に変換することができます。よって、以下のようなコードで文字列を分解することができます。
Array.from("🌕には𩸽")
["🌕", "に", "は", "𩸽"]
この方法も、古いWebブラウザでは使用できません。
実際に使用する時は、Babelなどで古いブラウザ向けに変換しましょう。
なお、古いブラウザ向けにArray.from関数だけを追加定義してくれるPolyfill(ポリフィル)という分類のライブラリもありますが、だいたいのものは文字列のサロゲートペアを考慮していないため、この用途では使えません。
MDNのページに載っている「ポリフィル」も対応していません。
このため、Polyfill(ポリフィル)の使用はオススメしません。
どうしてもPolyfill(ポリフィル)を使用したい場合は「mathiasbynens/Array.from」を使いましょう。コレはサロゲートペアに対応しています。
今日のWebブラウザは、全てArray.from関数に対応しています。唯一非対応だったInternet Explorerのサポートも2022年6月15日に終了し、2023年2月14日以降は開くことすらできなくなります。そのため、Array.from関数のPolyfillを使う機会はほとんど無いでしょう。
matchメソッドを使用した方法
ほとんどの場合、文字列の分解は上述した方法で達成できます。しかし、上述した方法を採用できない場合もあるでしょう。例えば、まだInternet Explorerをサポートしなければならない業務用アプリケーションとか、ECMAScript 2015(ECMAScript 6)すらサポートしていないものすごくマイナーなJavaScriptランタイムで使う必要があるコードとか。そのような場合は、matchメソッドを使うことでも文字列を分解することができます。
JavaScriptでのサロゲートペア文字列のメモ - Qiita #IV-II. サロゲートペアに対応した配列化
この記事にあるように、正規表現でサロゲートペアを考慮した文字を取得できるようにすれば解決します。
例えば以下のようにすると、文字列を正しく分割してくれます。
"🌕には𩸽".match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\s\S]/g) || []
"🌕には𩸽".match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\s\S]|^$/g).filter(Boolean)
["🌕", "に", "は", "𩸽"]
どちらのコードを採用しても、同じ結果を得ることができます。
もし、対象の文字列が空文字列ではない事が確実な場合は、以下のように単純化することもできます。
"🌕には𩸽".match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\s\S]/g)
-
ただし、ECMAScript 2015(ECMAScript 6)から追加された関数やメソッドはサロゲートペアに対応しています。例えば
String.fromCodePoint
関数やString.prototype.codePointAt
メソッドがこれに該当します。 ↩