サロゲートペア (Surrogate Pair) とは
この記事は約 4 分で読めます。
絵文字を 1 つ入力したのに「2 文字」とカウントされる現象。その正体がサロゲートペアです。Unicode の表現範囲を UTF-16 という古い設計で扱おうとすると、1 文字を表すのに 2 つの「片割れ」を使わざるを得ない場面があります。 この片割れの組のことをサロゲートペアと呼びます。絵文字や一部の漢字を扱うときに必ず登場する基礎概念です。
定義
サロゲートペアとは、UTF-16 において U+FFFF を超える Unicode コードポイントを 2 つの 16 ビット単位で表現する仕組みです。 前半を「上位サロゲート」(High Surrogate, U+D800〜U+DBFF)、後半を「下位サロゲート」(Low Surrogate, U+DC00〜U+DFFF) と呼び、 この 2 つが必ず対 (ペア) で出現します。
なぜ必要なのか
Unicode は U+0000 から U+10FFFF までの広大なコードポイント空間を持っています。これを表現するには 21 ビット必要です。 しかし UTF-16 は 1 単位 16 ビットの設計のため、U+FFFF までしか単独では表現できません。 この限界を超える文字 (絵文字の大半、一部の漢字、古代文字、数学記号など) を扱うために、上位と下位を組み合わせて拡張する仕組みが導入されました。
絵文字との関係
絵文字のコードポイントの多くは U+1F000 以降の領域にあり、UTF-16 ではサロゲートペアで表現されます。 たとえば 🌸 (U+1F338) は、UTF-16 では D83C DF38 という 2 つの 16 ビット単位として格納されます。 この事実が、JavaScript の文字列長カウントで絵文字 1 つが 2 になる現象の直接の原因です。
JavaScript の挙動
JavaScript の String は内部的に UTF-16 で表現されているため、.length はサロゲートペアを 2 とカウントします。
| 式 | 結果 | 意味 |
|---|---|---|
"あ".length | 1 | BMP 内なのでペア不要 |
"🌸".length | 2 | サロゲートペアで構成 |
[..."🌸"].length | 1 | コードポイント単位の分解 |
"🌸".codePointAt(0) | 127800 | 結合されたコードポイント値 |
UTF-8 / UTF-32 との違い
サロゲートペアは UTF-16 固有の仕組みです。UTF-8 は 1〜4 バイトの可変長で全コードポイントを表現でき、UTF-32 は 4 バイト固定で全コードポイントを 1 単位で扱います。 UTF-8 や UTF-32 を使う言語・処理系では、サロゲートペアの概念は登場しません。
実務での使われ方
- 文字数バリデーション: フォームの最大文字数を「人間が認識する文字単位」で数えたい場面でサロゲートペアの理解が必要
- テキスト処理: 部分文字列の切り出し、逆順処理、正規表現マッチで、サロゲートペアを 1 単位として扱う必要がある
- ファイル名・データベース: 古いシステムがサロゲートペアを正しく扱えず、絵文字を含むデータが破損するケース
- SNS の文字数カウント: 各プラットフォームのカウントロジックの差は、サロゲートペアと書記素クラスタの扱いの違いから生まれる
正しく数える方法
ユーザーが「1 文字」と認識する単位で数えたいなら、サロゲートペア単位ではなく書記素クラスタ単位で数える必要があります。 モダンな JavaScript では Intl.Segmenter が標準で利用可能で、絵文字や ZWJ シーケンスも適切に 1 単位として扱えます。
よくある誤解
- ❌ 「絵文字は全部 2 文字扱い」→ ✅ U+FFFF 以下にある絵文字 (一部の古い記号系) はペア不要で 1 文字
- ❌ 「サロゲートペアは UTF-8 でも使う」→ ✅ UTF-16 固有の仕組み
- ❌ 「上位サロゲート単独でも文字として有効」→ ✅ 単独では不正な Unicode 文字列 (Unpaired Surrogate)