絵文字の解剖学: ZWJシーケンス、肌色修飾子、国旗の仕組み
ZWJ、異体字セレクタ、地域インジケータを使った複合絵文字の構造。
シンプルな絵文字: 1コードポイント、1グリフ
最もシンプルな絵文字は単一のUnicodeコードポイントです。😀(U+1F600)、❤(U+2764)、☀(U+2600)はそれぞれ1コードポイントだけを占めます。しかし「シンプル」は相対的な概念で、たった1つの絵文字でもメモリ上では複数のユニットを占めることがあります。
U+FFFF以下の絵文字(☀ の U+2600 など)はUTF-16の1コードユニットに収まるため、"☀".length は 1 を返します。しかし現代のほとんどの絵文字は U+FFFF を超える追加多言語面(SMP)に存在します。U+1F600 の 😀 はUTF-16でサロゲートペアが必要なため、"😀".length は 2 を返します。たった1コードポイントなのにです。
この違いは文字列のインデックスアクセスを行うすべてのコードに影響します。最も基本的な絵文字でさえ、追加面に存在する場合は素朴な文字列処理を狂わせる可能性があります。重要な洞察:1つの視覚的文字は、1単位のストレージを意味しません。
テキスト vs 絵文字表示: VS15 と VS16
一部のコードポイントには二面性があります。☺(U+263A)は絵文字よりずっと前からUnicodeに存在していた記号です。絵文字が普及すると、同じコードポイントにカラフルな絵文字レンダリングが加わりました。Unicodeは2つの不可視な異体字セレクタでこの曖昧さを解決します:
| セレクタ | コードポイント | 効果 | 例 |
|---|---|---|---|
| VS15(テキスト) | U+FE0E | モノクロ/テキスト表示を強制 | ☺︎ |
| VS16(絵文字) | U+FE0F | カラフルな絵文字表示を強制 | ☺️ |
☺︎☺️ には見た目が異なる2つの文字が含まれていますが、ベースのコードポイントは同一で、末尾の異体字セレクタだけが異なります。異体字セレクタがない場合のデフォルト表示はプラットフォームと文脈に依存します。ほとんどのスマートフォンでは ☺ は絵文字スタイルがデフォルトですが、多くのターミナルエミュレータではテキストスタイルがデフォルトです。
VS16(U+FE0F)はZWJシーケンスで特に重要です。レインボーフラッグ 🏳️🌈 では、白旗の後にVS16が配置され、ZWJで虹と結合される前に絵文字スタイルでのレンダリングが保証されます。VS16を除去するとシーケンス全体が壊れる可能性があります。
肌色修飾子: Fitzpatrick スケール
Unicode 8.0 で、皮膚科学のフィッツパトリックスケールに基づく5つの肌色修飾子が導入されました。U+1F3FB から U+1F3FF のコードポイントで、対応するベース絵文字の直後に配置して肌色を変更します:
| 修飾子 | コードポイント | Fitzpatrick タイプ | 例 |
|---|---|---|---|
| 🏻 | U+1F3FB | タイプ 1-2(明るい) | 👍🏻 |
| 🏼 | U+1F3FC | タイプ 3(やや明るい) | 👍🏼 |
| 🏽 | U+1F3FD | タイプ 4(中間) | 👍🏽 |
| 🏾 | U+1F3FE | タイプ 5(やや暗い) | 👍🏾 |
| 🏿 | U+1F3FF | タイプ 6(暗い) | 👍🏿 |
肌色付き絵文字は2コードポイントで1書記素クラスタを形成します。👍🏽 = U+1F44D(サムズアップ)+ U+1F3FD(中間の肌色)。UTF-16では両コードポイントとも追加面にあり、各サロゲートペアが必要なため、"👍🏽".length は 4、[..."👍🏽"].length は 2 ですが、Intl.Segmenter は正しく 1 と報告します。
すべての絵文字が肌色をサポートするわけではありません。対応していないベース(車やピザなど)に修飾子を適用すると、修飾子が別の色付き四角形として表示されるだけです。対応するベースの完全なリストは Unicode の emoji-data.txt の Emoji_Modifier_Base プロパティで定義されています。
ZWJシーケンス: 絵文字を接着する
ゼロ幅接合子(Zero Width Joiner、U+200D)は、複数の絵文字を1つの書記素クラスタに「接着」する不可視文字です。家族絵文字 👨👩👧👦 は4つの個別の絵文字が3つのZWJで結合されています:
👨👩👧👦 = 👨 + ZWJ + 👩 + ZWJ + 👧 + ZWJ + 👦
= U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466
= 7コードポイント → 11 UTF-16コードユニット → 1書記素クラスタZWJシーケンスは多種多様な現代の絵文字を支えています。職業絵文字は人物と道具を組み合わせます:👩🚀(女性 + ZWJ + ロケット)、👨💻(男性 + ZWJ + ノートPC)、👩🔬(女性 + ZWJ + 顕微鏡)。カップル絵文字は2人の人物をハートで結合:👩❤️👨。レインボーフラッグは白旗と虹を結合:🏳️🌈 = 🏳 + VS16 + ZWJ + 🌈。
プラットフォームが特定のZWJシーケンスを認識しない場合はどうなるでしょうか?フォールバックは優雅で、構成要素の絵文字が個別に並んで表示されます。これにより、公式に標準化される前から新しいZWJの組み合わせを提案・使用でき、古いシステムではコンポーネントがそのまま表示されるだけです。
国旗絵文字: 地域インジケータのペア
国旗絵文字は独立した文字ではありません。地域インジケータ記号(U+1F1E6 から U+1F1FF)のペアで構成されます。これは A から Z に対応する26文字のセットで、2つの地域インジケータが ISO 3166-1 alpha-2 の国コードに基づいて国旗を形成します:
| 国旗 | インジケータ | 国コード | UTF-16 length |
|---|---|---|---|
| 🇯🇵 | 🇯 (U+1F1EF) + 🇵 (U+1F1F5) | JP(日本) | 4 |
| 🇺🇸 | 🇺 (U+1F1FA) + 🇸 (U+1F1F8) | US(アメリカ) | 4 |
| 🇬🇧 | 🇬 (U+1F1EC) + 🇧 (U+1F1E7) | GB(イギリス) | 4 |
各地域インジケータは追加面(U+FFFFを超える)にあるため、UTF-16ではサロゲートペアが必要です。1つの国旗 = 2コードポイント = 4 UTF-16コードユニット。3つの国旗を並べると:"🇯🇵🇺🇸🇬🇧".length は 12 を返しますが、書記素クラスタは3つだけです。
このペアリング方式では、国旗を不注意に連結すると予期しない結果が生じます。🇯🇵🇺🇸 を 🇵 と 🇺 の間で分割すると、これら2つのインジケータが結合して 🇵🇺(意図しない国の国旗、または認識されないペア)を形成する可能性があります。国旗を含むテキストを扱う際に書記素クラスタ対応の分割が不可欠な理由です。
emoji.length が常に驚きをもたらす理由
すべてをまとめると、JavaScriptの .length が様々な絵文字構成に対して返す値は次の通りです:
| 絵文字 | 表示 | .length | コードポイント | 書記素クラスタ |
|---|---|---|---|---|
| シンプル(BMP) | ☀ | 1 | 1 | 1 |
| シンプル(SMP) | 😀 | 2 | 1 | 1 |
| VS16付き | ☺️ | 2 | 2 | 1 |
| 肌色付き | 👍🏽 | 4 | 2 | 1 |
| 国旗 | 🇯🇵 | 4 | 2 | 1 |
| ZWJ 家族 | 👨👩👧👦 | 11 | 7 | 1 |
| ZWJ 国旗 | 🏳️🌈 | 6 | 4 | 1 |
パターンは明白です:「1文字」に見えるものすべてが、.length では 1 から 11 以上の範囲になります。ユーザーが知覚する「文字」を確実に数える唯一の方法は、Intl.Segmenter で書記素クラスタを数えることです。
// 唯一の信頼できる絵文字対応の文字数カウント
const count = (s: string) =>
[...new Intl.Segmenter().segment(s)].length;
count("👨👩👧👦"); // 1
count("🇯🇵🇺🇸🇬🇧"); // 3
count("☺︎☺️"); // 2
count("👍🏽"); // 1文字列の反転も一般的な落とし穴です。[..."👨👩👧👦"].reverse().join("") は個々のコードポイント(ZWJ文字を含む)を逆順にするため、意図したグルーピングが壊れてしまいます。国旗絵文字はさらに深刻で、🇯🇵 をコードポイントレベルで反転すると 🇵🇯 になり、まったく別の国旗になります。常にコードユニットやコードポイントではなく、書記素クラスタ単位で操作してください。