JavaScript で �（置換文字）が表示される理由と回避方法

# はじめに
JavaScript で文字列を操作していると、黒いひし形に？のマークの「�」（`U+FFFD`）が表示された経験が一度はあると思います。（ブラウザによっては 黒で縁取られた四角形が表示されるかもしれません。）

これは「**置換文字**」（Replacement Character）と呼ばれ、不正な文字を置き換えるために使用されます。

この記事では、JavaScript で「�」が表示される原因と、「�」を表示させないための方法について詳しく解説します。前提知識として **Unicode** と **UTF-16** についても説明します。


# Unicode と UTF-16
JavaScript は**文字コード**として **Unicode** を採用し、**符号化方式**として **UTF-16** を採用しています。 

## 文字コード
**文字コード**は、コンピュータ上で文字を扱うために、文字に番号を割り振って定義したものです。`ASCII` や `Unicode`、`Shift_JIS` などの種類があります。近年では Unicode が主流となりつつあり、様々なシステムで広く利用されています。

**Unicode** は、世界中のあらゆる言語で使用されるすべての文字を、固有の**識別番号**で管理する国際規格です。この識別番号のことを**コードポイント**と呼びます。
例えば、`a` の**コードポイント**は 16 進数表記で `61` です（10 進数表記: `97`）。

JavaScript で文字列を記述する際、通常の文字表現だけでなく、コードポイントを用いて記述する方法があります。`\uXXXX` のように Unicode のコードポイントを直接記述する方法です。`XXXX` には、表現したい文字のコードポイントを 16 進数で記述します。
例えば、`'a'` と記述する代わりに `'\u0061'` と記述することが可能です。

## 符号化方式
**符号化方式**は、**コードポイントをビット列に変換**する方法です。コンピュータ上では、あらゆるデータが 2 種類の電気信号 ( `0` と `1` に対応 ）で表現されています。そのため、コンピュータ上で文字を扱う際にはコードポイントをビット列に変換する必要があります。符号化方式には `UTF-8` や`UTF-16` などの種類があります。

**UTF-16** は文字を **16 ビット単位**で表現する符号化方式です。

```:a を UTF-16 で表現
`a` のコードポイントは 10 進数表記で `61` で 2 進数表記で `110 0001` です。

UTF-16 は文字を 16 ビット単位で表現するため、以下のようにメモリに格納されます。
0000 0000 0110 0001
```
16 ビット（= 2 バイト）では、理論的には 65,536（2 の 16 乗）種類の文字を表すことができます。コードポイントが `0` から `FFFF`（10 進数表記で 65,535） の範囲は**基本多言語面**（BMP）と呼ばれます。BMP には、英数字、記号、大部分のヨーロッパ言語、アジア言語の文字などが含まれています。


UTF-16 では BMP 内の文字は 16 ビットで表現しますが、BMP 外の文字は**サロゲートペア**と呼ばれる 2 つの 16 ビット値の組み合わせ（32 ビット）を用いて表現します。


## サロゲートペア
**サロゲートペア**（surrogate pair）は、BMP 外の文字を表現するために使用されます。これらの文字は 2 つの 16 ビット値（サロゲートペア）を組み合わせて表現されます。

サロゲートペアは、以下の 2 つの値で構成されます。

- **上位サロゲート**: `D800` ~ `DBFF` の範囲のコードポイント
- **下位サロゲート**: `DC00` ~ `DFFF` の範囲のコードポイント

上位サロゲートと下位サロゲートを組み合わせることで、1,048,576（2 の 20 乗）種類の文字を追加で表現することができます。

上位サロゲートと下位サロゲートは、単独で有効な文字として定義されていません。つまり、`D842` や `DFB7` などの値を単独で使うことはできません。

```JavaScript
const surrogatePair = '𠮷'; // BMP 外の文字
console.log(surrogatePair.length); // 2
console.log(surrogatePair.charCodeAt(0)); // 55362 (16進数表記: D842)
console.log(surrogatePair.charCodeAt(1)); // 57271 (16進数表記: DFB7)
```

#  置換文字「�」が表示される原因
「�」が表示される主な原因は以下の通りです。
- サロゲートペアの不適切な処理
- Unicode の不適切なエンコーディングやデコーディング


# 不適切な処理
JavaScript で文字列を処理する際に、**`charAt` メソッド**や**インデックス**を使う場合がありますが、これらの方法はユニコードのサロゲートペアや結合文字を正しく扱うことができないため、注意が必要です。
```javascript
let text = 'foo';
console.log(text[0]);        // 'f'
console.log(text.charAt(1)); // 'o'
```
このコードは単純な BMP 内の文字列を扱っている場合には問題ありません。

しかし、Unicode の絵文字など、**サロゲートペア**を含む文字列を処理する場合には問題が発生します。
```javascript
'😊'[0];        // '\uD83D'
'😊'.charAt(1); // '\uDE0A'

// \uD83D も \uDE0A も console.log で表示する場合、� と表示されます。
const str = '😊';
for (let i = 0; i < str.length; i++) {
    console.log(str[i]);
}
// �
// �

// Web ページ上で表示する場合も、� と表示されます。
// （Safari や Firefox では 黒で縁取り四角形が表示されるかもしれません。）
document.getElementById('foo').innerText = "😊".charAt(0);
```
上記のコードでは、絵文字「😊」はサロゲートペアで表現されています。しかし、`charAt` メソッドやインデックスを使用して文字を取得しようとすると、サロゲートペアの一部のみを取得してしまい、結果的に不完全な Unicode 文字となり、「�」として表示されます。


# サロゲートペアの正しい処理
**サロゲートペア**は、UTF-16 エンコーディングで BMP 外の文字を表現するための方法です。




サロゲートペアを正しく扱うためには、`for...of` ループやスプレッド構文（`[... ]`）、`Array.from` メソッドを使用します。

```javascript
const str = 'A😊';

// for...of ループ
for (const char of str) {
    console.log(char); // 正しく 'A' と '😊' が表示される
}

// スプレッド構文
const chars = [...str];
console.log(chars); // 正しく ['A', '😊'] と表示される

// Array.from
const charsFromArray = Array.from(str);
console.log(charsFromArray); // 正しく ['A', '😊'] と表示される
```

# リージョナルインディケータシンボル文字の正しい扱い
**リージョナルインディケータシンボル文字**（Regional Indicator Symbol Letters）も同様に、正しい方法で処理する必要があります。


リージョナルインディケータシンボル文字は、ユニコードで国旗の絵文字を表現するために使用される特殊な文字です。これらの文字は、`1F1E6` から `1F1FF` までの範囲に定義されており、A から Z までの英字に対応しています。

以下のように 2 つのリージョナルインディケータシンボル文字を組み合わせて旗を表現します。

- 🇯🇵（日本）：`1F1EF`（🇯） + `1F1F5`（🇵）
- 🇺🇸（アメリカ）：`1F1FA`（🇺） + `1F1F8`（🇸）
- 🇬🇧（イギリス）：`1F1EC`（🇬） + `1F1E7`（🇧）

`1F1E6` から `1F1FF` までの範囲は BMP 外の文字であり、サロゲートペアを使用して表現します。各サロゲートペアは 2 つの 16 ビット値で構成されており、1 つのリージョナルインディケータシンボル文字は 32 ビット（4バイト）で表現されます。

例えば、🇺🇸 は `1F1FA`（🇺） と `1F1F8`（🇸）で構成されますが、`1F1FA`（🇺）は `D83C` と `DDFA`、`1F1F8`（🇸）は `D83C` と `DDF8` のサロゲートペアを用いて表現します。つまり、🇺🇸 は `D83C DDFA D83C DDF8` の 32 バイトで表現されます。


```javascript
const str = '🇺🇸'; // '\uD83C\uDDFA\uD83C\uDDF8'

// 不適切な処理
console.log(str.charAt(0)); // '\uD83C' (不完全な文字)
// �
console.log(str.charAt(1)); // '\uDDFA' (不完全な文字)
// �


// 正しい文字列処理
const chars = [...str];

console.log(chars); 
// ['🇺', '🇸']

// join メソッドで元に戻すことができます
console.log(chars.join(''))
// 🇺🇸
```
この例では、スプレッド構文を使用して文字列を分割することで、サロゲートペアを正しく処理し、国旗絵文字を 2 つのリージョナルインディケータシンボル文字として正しく扱うことができます。

# ゼロ幅結合子（ZWJ）の処理
**ゼロ幅接合子**（`\u200D`）を含む文字列も正しく処理する必要があります。

ゼロ幅結合子は、複数の絵文字を結合して 1 つの絵文字として表示するために使用されます。これにより、例えば家族の絵文字や複合絵文字が作成されます。

```javascript
// 🐻 と ❄ を \u200D で結合すると 🐻‍❄ になります
console.log('🐻\u200D❄') // 🐻‍❄

const str = '👩‍👩‍👧‍👦';

// ZWJ を含む文字列の処理
const chars = [...str];
console.log(chars); // ['👩', '‍', '👩', '‍', '👧', '‍', '👦']

// join メソッドで元に戻すことができます
console.log(chars.join('')) // 👩‍👩‍👧‍👦
```

# 不適切なエンコーディングやデコーディング
`TextEncoder` と `TextDecoder` を使用できます。以下は、UTF-8 でエンコードされた文字列を ISO-8859-1（latin1）でデコードする例です。このような誤ったエンコーディングによって、文字列が正しく表示されず、置換文字が現れることがあります。

```javascript
const text = 'こんにちは';
const encoder = new TextEncoder();
// UTF-8 でエンコードされた文字の配列
const utf8Array = encoder.encode(text);

// ISO-8859-1（latin1）でデコードする（不適切な処理）
const decoder = new TextDecoder('iso-8859-1');
const incorrectText = decoder.decode(utf8Array);

// 正しい UTF-8 でのデコード
const correctDecoder = new TextDecoder('utf-8');
const correctText = correctDecoder.decode(utf8Array);

// 結果の表示
console.log('UTF-8エンコード: ', utf8Array); 
// UTF-8エンコード: [227, 129, 147, 227, 130, 147, 227, 129, 171, 227, 129, 161, 227, 129, 175]
console.log('不適切なデコード: ', incorrectText);
// 不適切なデコード: ã�“ã‚“ã�«ã�¡ã�¯
// （ブラウザによっては、`不適切なデコード: ã“ã‚“ã«ã¡ã¯` と表示される場合もあります）
console.log('正しいデコード: ', correctText); 
// 正しいデコード: こんにちは
```

# まとめ

JavaScriptで「�」が表示される問題を回避するためには、Unicode 文字列を正しく処理することが重要です。

`for...of` ループ、スプレッド構文、`Array.from` メソッドを使用することで、サロゲートペア、結合文字、ゼロ幅結合子などの特殊な文字を正しく扱うことができます。これにより、Unicode 文字列を含む文字列操作でのバグや表示の問題を回避できます。