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


JavaScriptで<ruby>&#x29E3D;<rp>(</rp><rt>ホッケの漢字</rt><rp>)</rp></ruby>や<ruby>&#x1F315;<rp>(</rp><rt>満月の絵文字</rt><rp>)</rp></ruby>を扱おうとすると、やや面倒な事になります。

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

[splitメソッド]: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/split

```js:コード
"満月の夜にホッケ食べたい".split("")
```

```js:生成されるデータ
["満", "月", "の", "夜", "に", "ホ", "ッ", "ケ", "食", "べ", "た", "い"]
```

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

```js:コード
"🌕の夜に𩸽食べたい".split("")
```

```js:生成されるデータ
["�", "�", "の", "夜", "に", "�", "�", "食", "べ", "た", "い"]
```

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

## 原因

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

これは、<ruby>&#x29E3D;<rp>(</rp><rt>ホッケの漢字</rt><rp>)</rp></ruby>や<ruby>&#x1F315;<rp>(</rp><rt>満月の絵文字</rt><rp>)</rp></ruby>が「サロゲートペア」に該当する文字だから起きてしまう現象です。

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

<table>
  <tr>
    <th style="text-align:center">文字</th>
    <td style="text-align:center">満</td>
    <td style="text-align:center">月</td>
    <td style="text-align:center">に</td>
    <td style="text-align:center">は</td>
    <td style="text-align:center">ホ</td>
    <td style="text-align:center">ッ</td>
    <td style="text-align:center">ケ</td>
  </tr>
  <tr>
    <th style="text-align:center">Unicode</th>
    <td style="text-align:center"><code>U+6E80</code></td>
    <td style="text-align:center"><code>U+6708</code></td>
    <td style="text-align:center"><code>U+306B</code></td>
    <td style="text-align:center"><code>U+306F</code></td>
    <td style="text-align:center"><code>U+30DB</code></td>
    <td style="text-align:center"><code>U+30C3</code></td>
    <td style="text-align:center"><code>U+30B1</code></td>
  </tr>
  <tr>
    <th style="text-align:center">UTF-16</th>
    <td style="text-align:center"><code>U+6E80</code></td>
    <td style="text-align:center"><code>U+6708</code></td>
    <td style="text-align:center"><code>U+306B</code></td>
    <td style="text-align:center"><code>U+306F</code></td>
    <td style="text-align:center"><code>U+30DB</code></td>
    <td style="text-align:center"><code>U+30C3</code></td>
    <td style="text-align:center"><code>U+30B1</code></td>
  </tr>
  <tr>
    <th style="text-align:center">データ（UTF-16BE）</th>
    <td style="text-align:center"><code>0x6E&nbsp;0x80</code></td>
    <td style="text-align:center"><code>0x67&nbsp;0x08</code></td>
    <td style="text-align:center"><code>0x30&nbsp;0x6B</code></td>
    <td style="text-align:center"><code>0x30&nbsp;0x6F</code></td>
    <td style="text-align:center"><code>0x30&nbsp;0xDB</code></td>
    <td style="text-align:center"><code>0x30&nbsp;0xC3</code></td>
    <td style="text-align:center"><code>0x30&nbsp;0xB1</code></td>
  </tr>
  <tr>
    <th style="text-align:center">データ（UTF-16LE）</th>
    <td style="text-align:center"><code>0x80&nbsp;0x6E</code></td>
    <td style="text-align:center"><code>0x08&nbsp;0x67</code></td>
    <td style="text-align:center"><code>0x6B&nbsp;0x30</code></td>
    <td style="text-align:center"><code>0x6F&nbsp;0x30</code></td>
    <td style="text-align:center"><code>0xDB&nbsp;0x30</code></td>
    <td style="text-align:center"><code>0xC3&nbsp;0x30</code></td>
    <td style="text-align:center"><code>0xB1&nbsp;0x30</code></td>
  </tr>
</table>

ところで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バイトで表現するようになっています。
これがサロゲートペアです。

<table>
  <tr>
    <th style="text-align:center">文字</th>
    <td style="text-align:center" colspan="2">🌕</td>
    <td style="text-align:center">に</td>
    <td style="text-align:center">は</td>
    <td style="text-align:center" colspan="2">𩸽</td>
  </tr>
  <tr>
    <th style="text-align:center">Unicode</th>
    <td style="text-align:center" colspan="2"><code>U+1F315</code></td>
    <td style="text-align:center"><code>U+306B</code></td>
    <td style="text-align:center"><code>U+306F</code></td>
    <td style="text-align:center" colspan="2"><code>U+29E3D</code></td>
  </tr>
  <tr>
    <th style="text-align:center">UTF-16</th>
    <td style="text-align:center"><code>U+D83C</code></td>
    <td style="text-align:center"><code>U+DF15</code></td>
    <td style="text-align:center"><code>U+306B</code></td>
    <td style="text-align:center"><code>U+306F</code></td>
    <td style="text-align:center"><code>U+D867</code></td>
    <td style="text-align:center"><code>U+DE3D</code></td>
  </tr>
  <tr>
    <th style="text-align:center">データ（UTF-16BE）</th>
    <td style="text-align:center" colspan="2"><code>0xD8&nbsp;0x3C&nbsp;0xDF&nbsp;0x15</code></td>
    <td style="text-align:center"><code>0x30&nbsp;0x6B</code></td>
    <td style="text-align:center"><code>0x30&nbsp;0x6F</code></td>
    <td style="text-align:center" colspan="2"><code>0xD8&nbsp;0x67&nbsp;0xDE&nbsp;0x3D</code></td>
  </tr>
  <tr>
    <th style="text-align:center">データ（UTF-16LE）</th>
    <td style="text-align:center" colspan="2"><code>0x3C&nbsp;0xD8&nbsp;0x15&nbsp;0xDF</code></td>
    <td style="text-align:center"><code>0x6B&nbsp;0x30</code></td>
    <td style="text-align:center"><code>0x6F&nbsp;0x30</code></td>
    <td style="text-align:center" colspan="2"><code>0x67&nbsp;0xD8&nbsp;0x3D&nbsp;0xDE</code></td>
  </tr>
</table>

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

[^JavaScriptはサロゲートペアに非対応]: ただし、ECMAScript 2015(ECMAScript 6)から追加された関数やメソッドはサロゲートペアに対応しています。例えば[`String.fromCodePoint`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/fromCodePoint)関数や[`String.prototype.codePointAt`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/codePointAt)メソッドがこれに該当します。

```js:コード
"🌕には𩸽".split("")
```

```js:生成されるデータ
["�", "�", "に", "は", "�", "�"]
```

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

## 対応方法

### 反復処理プロトコルを使用する方法

ECMAScript 2015(ECMAScript 6)からは、[反復処理プロトコル]というものが追加されています。これは、「`Symbol.iterator`変数に入っているシンボル型の値」のプロパティを持つ値の事です。このプロパティにはメソッドが割り当てられており、このメソッドを呼ぶことで返ってくる反復子オブジェクトを使うことで、配列のような「複数の値を格納したオブジェクト」から値を1つづつ取り出せます。ECMAScript 2015以降は、配列も文字列も[反復処理プロトコル]に対応しています。

[反復処理プロトコル]: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Iteration_protocols

そして重要な事は、**文字列の[反復処理プロトコル]を使うことで、サロゲートペアを考慮した文字の取り出しが可能になる**のです。例えば、以下のようなコードを記述することで、サロゲートペアを考慮した「1文字ずつに分解した配列」を得ることができます。

```js:コード
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("🌕には𩸽")
```

```js:生成されるデータ
["🌕", "に", "は", "𩸽"]
```

しかし、**こんな長いコードを書く必要はありません**。ECMAScript 2015には、反復処理プロトコルに対応したオブジェクトを配列に変換するための構文や関数が存在します。

#### [スプレッド構文（スプレッド演算子）]を使用した方法

[スプレッド構文（スプレッド演算子）]: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Spread_syntax

ECMAScript 2015(ECMAScript 6)から追加された[スプレッド構文（スプレッド演算子）]を使用することで、上述した「[反復処理プロトコル]に対応するオブジェクト」を配列に変換することができます。つまり、以下のように記述することで文字列を簡単に分解することができます。

```js:コード
[..."🌕には𩸽"]
```

```js:生成されるデータ
["🌕", "に", "は", "𩸽"]
```

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

[Babel]: https://babeljs.io

<ruby>今日<rp>(</rp><rt>こんにち</rt><rp>)</rp></ruby>のWebブラウザは、[全てスプレッド構文に対応しています](https://caniuse.com/mdn-javascript_operators_spread_spread_in_arrays)。唯一非対応だったInternet Explorerのサポートも[2022年6月15日に終了し、2023年2月14日以降は開くことすらできなくなります](https://support.microsoft.com/ja-jp/windows/internet-explorer-%E3%81%AE%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89-d49e1f0d-571c-9a7b-d97e-be248806ca70)。そのため、気兼ねなく使えるでしょう。

#### [Array.from関数]を使用した方法

[Array.from関数]: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/from

ECMAScript 2015(ECMAScript 6)から追加された[Array.from関数]も、上述した「[反復処理プロトコル]に対応するオブジェクト」を配列に変換することができます。よって、以下のようなコードで文字列を分解することができます。

```js:コード
Array.from("🌕には𩸽")
```

```js:生成されるデータ
["🌕", "に", "は", "𩸽"]
```

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

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

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

<ruby>今日<rp>(</rp><rt>こんにち</rt><rp>)</rp></ruby>のWebブラウザは、[全てArray.from関数に対応しています](https://caniuse.com/mdn-javascript_builtins_array_from)。唯一非対応だったInternet Explorerのサポートも[2022年6月15日に終了し、2023年2月14日以降は開くことすらできなくなります](https://support.microsoft.com/ja-jp/windows/internet-explorer-%E3%81%AE%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89-d49e1f0d-571c-9a7b-d97e-be248806ca70)。そのため、[Array.from関数]のPolyfillを使う機会はほとんど無いでしょう。

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

[matchメソッド]: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/match

ほとんどの場合、文字列の分解は上述した方法で達成できます。しかし、上述した方法を採用できない場合もあるでしょう。例えば、まだInternet Explorerをサポートしなければならない業務用アプリケーションとか、ECMAScript 2015(ECMAScript 6)すらサポートしていないものすごくマイナーなJavaScriptランタイムで使う必要があるコードとか。そのような場合は、[matchメソッド]を使うことでも文字列を分解することができます。

[JavaScriptでのサロゲートペア文字列のメモ - Qiita #IV-II. サロゲートペアに対応した配列化](https://qiita.com/YusukeHirao/items/2f0fb8d5bbb981101be0#iv-ii-%E3%82%B5%E3%83%AD%E3%82%B2%E3%83%BC%E3%83%88%E3%83%9A%E3%82%A2%E3%81%AB%E5%AF%BE%E5%BF%9C%E3%81%97%E3%81%9F%E9%85%8D%E5%88%97%E5%8C%96)

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

```js:コード
"🌕には𩸽".match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\s\S]/g) || []
```

```js:コード
"🌕には𩸽".match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\s\S]|^$/g).filter(Boolean)
```

```js:生成されるデータ
["🌕", "に", "は", "𩸽"]
```

どちらのコードを採用しても、同じ結果を得ることができます。

もし、対象の文字列が空文字列ではない事が確実な場合は、以下のように単純化することもできます。

```js:コード
"🌕には𩸽".match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\s\S]/g)
```