Unicode 與 Emoji 編碼完全指南:從碼位到渲染,搞懂字元編碼的一切

加密編碼編碼、雜湊與 QR Code

Unicode:統一所有字元的編碼標準

Unicode 為世界上幾乎所有文字系統的每個字元分配唯一編號(碼位)。

概念 說明 範例
碼位 (Code Point) 字元的唯一編號 U+4E2D (中)
碼平面 每 65536 個碼位為一個平面 BMP (0 號平面)
BMP 基本多文種平面,U+0000~U+FFFF 大部分常用字元
增補平面 U+10000~U+10FFFF Emoji、古文字、生僻字

一、UTF 編碼家族

編碼 位元組長度 "中" 的編碼 特點
UTF-8 1-4 位元組 E4 B8 AD 可變長,相容 ASCII,Web 首選
UTF-16 2 或 4 位元組 4E2D 可變長,JS 內部使用
UTF-32 固定 4 位元組 00004E2D 定長,記憶體佔用大

UTF-8 編碼規則

碼位範圍 位元組數 編碼模板
U+0000 ~ U+007F 1 0xxxxxxx
U+0080 ~ U+07FF 2 110xxxxx 10xxxxxx
U+0800 ~ U+FFFF 3 1110xxxx 10xxxxxx 10xxxxxx
U+10000 ~ U+10FFFF 4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

編碼範例

字元: 中
碼位: U+4E2D = 0100 1110 0010 1101

UTF-8 (3位元組):
  1110xxxx 10xxxxxx 10xxxxxx
  1110 0100 10 111000 10 101101
  = E4 B8 AD

UTF-16 (2位元組):
  4E2D

UTF-32 (4位元組):
  00004E2D

二、JavaScript 的 Unicode 陷阱

字串長度不準

'中'.length              // 1 ✅
'𠮷'.length             // 2 ❌ (增補平面字元,代理對)
'👨‍👩‍👧‍👦'.length           // 11 ❌ (Emoji ZWJ 序列)
'🏳️‍🌈'.length             // 6 ❌ (旗幟 Emoji)

原因:JS 使用 UTF-16

增補平面的字元(碼位 > U+FFFF)需要代理對(2 個 UTF-16 碼元)表示:

字元: 𠮷 (U+20BB7)
UTF-16 代理對: D842 DF87
JS: length = 2 (兩個碼元)

正確處理方式

// 方法一:展開運算子
[...'𠮷'].length           // 1 ✅
[...'👨‍👩‍👧‍👦'].length         // 1 ✅ (但 ZWJ 序列視為一個)

// 方法二:Array.from
Array.from('𠮷').length    // 1 ✅

// 方法三:for...of
for (const char of '𠮷👨‍👩‍👧‍👦') {
  console.log(char);  // 逐字元迭代
}

碼位操作

// 取得碼位
'中'.codePointAt(0)        // 20013 (0x4E2D)
'𠮷'.codePointAt(0)        // 134047 (0x20BB7)

// 從碼位建立字元
String.fromCodePoint(0x4E2D)  // '中'
String.fromCodePoint(0x20BB7) // '𠮷'

// 注意:charCodeAt 無法處理增補平面
'𠮷'.charCodeAt(0)         // 55362 (僅第一個代理)

三、Emoji 編碼深度解析

Emoji 的四種類型

類型 範例 碼位 長度
基礎 Emoji 😀 U+1F600 1 (碼位)
修飾符 Emoji 👩🏽 U+1F469 + U+1F3FD 2 (碼位)
ZWJ 序列 👨‍👩‍👧‍👦 多碼位 + ZWJ 7 (碼位)
旗幟 Emoji 🇨🇳 U+1F1E8 + U+1F1F3 2 (碼位)

零寬連接符 (ZWJ)

ZWJ (U+200D) 將多個 Emoji 連接為一個複合 Emoji:

👨 (U+1F468) + ZWJ (U+200D) + 👩 (U+1F469) + ZWJ (U+200D) + 👧 (U+1F467) + ZWJ (U+200D) + 👦 (U+1F466)
= 👨‍👩‍👧‍👦 (一家四口)

膚色修飾符

膚色修飾符 (U+1F3FB ~ U+1F3FF) 改變 Emoji 的膚色:

👩 (U+1F469) + 🏽 (U+1F3FD) = 👩🏽 (中等膚色女性)
修飾符 碼位 膚色
🏻 U+1F3FB 淺膚色
🏼 U+1F3FC 中淺膚色
🏽 U+1F3FD 中等膚色
🏾 U+1F3FE 中深膚色
🏿 U+1F3FF 深膚色

旗幟 Emoji

旗幟由兩個區域指示符號組成(ISO 3166-1 alpha-2):

🇨 (U+1F1E8) + 🇳 (U+1F1F3) = 🇨🇳 (中國)
🇺 (U+1F1FA) + 🇸 (U+1F1F8) = 🇺🇸 (美國)
🇯 (U+1F1EF) + 🇵 (U+1F1F5) = 🇯🇵 (日本)

四、變體選擇符

VS16 (U+FE0F) 將文字呈現字元轉為 Emoji 呈現:

❤ (U+2764)           → 文字呈現:❤ (黑色心形)
❤️ (U+2764 + U+FE0F) → Emoji 呈現:❤️ (紅色心形)
字元 無 VS16 有 VS16
☕ ☕ (文字) ☕️ (Emoji)
⚡ ⚡ (文字) ⚡️ (Emoji)
❤ ❤ (文字) ❤️ (Emoji)

五、使用工具庫處理 Unicode

Unicode 編解碼

  1. 開啟 Unicode 編解碼工具
  2. 輸入文字或碼位
  3. 查看轉換結果:
    • 文字 → Unicode 跳脫序列 (\u4E2D)
    • 碼位 → 字元
    • UTF-8/16/32 位元組序列

Emoji 編碼

  1. 開啟 Emoji 編碼工具
  2. 輸入 Emoji
  3. 查看完整編碼資訊:
    • 碼位序列
    • UTF-8 位元組
    • ZWJ/修飾符分解
    • CLDR 短名稱

六、常見陷阱與解決

陷阱一:字串截斷

// 錯誤:截斷可能切斷代理對
text.substring(0, 10)  // 可能產生無效字元

// 正確:按碼位截斷
function sliceByCodePoint(str, start, end) {
  const chars = [...str];
  return chars.slice(start, end).join('');
}

陷阱二:字串反轉

// 錯誤:反轉破壞組合字元
'👨‍👩‍👧‍👦'.split('').reverse().join('')  // 亂碼

// 正確:按碼位反轉
[...'👨‍👩‍👧‍👦'].reverse().join('')  // ✅

陷阱三:正規表達式比對

// 錯誤:. 不比對增補平面字元
/^.$/.test('𠮷')  // false

// 正確:使用 u 旗標
/^.$/u.test('𠮷')  // true

陷阱四:排序

// 預設排序按 UTF-16 碼元
['中', '𠮷', 'a'].sort()  // 可能不按預期

// 正確:按碼位排序
['中', '𠮷', 'a'].sort((a, b) =>
  a.codePointAt(0) - b.codePointAt(0)
)

七、實用 API

// 正規化(處理組合字元等價)
'café'.normalize('NFC')   // café (組合)
'café'.normalize('NFD')   // café (分解)

// 偵測 Emoji
function isEmoji(char) {
  const code = char.codePointAt(0);
  return (code >= 0x1F600 && code <= 0x1F64F) ||  // Emoticons
         (code >= 0x1F300 && code <= 0x1F5FF) ||  // Misc Symbols
         (code >= 0x1F680 && code <= 0x1F6FF) ||  // Transport
         (code >= 0x1F900 && code <= 0x1F9FF);    // Supplemental
}

// 取得字元的真實長度
function graphemeLength(str) {
  const segmenter = new Intl.Segmenter('zh', { granularity: 'grapheme' });
  return [...segmenter.segment(str)].length;
}

graphemeLength('👨‍👩‍👧‍👦')  // 1 ✅
graphemeLength('🏳️‍🌈')    // 1 ✅
#Unicode#Emoji#UTF-8#字符编码#码点