Unicode 與 Emoji 編碼完全指南:從碼位到渲染,搞懂字元編碼的一切
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 編解碼
- 開啟 Unicode 編解碼工具
- 輸入文字或碼位
- 查看轉換結果:
- 文字 → Unicode 跳脫序列 (
\u4E2D) - 碼位 → 字元
- UTF-8/16/32 位元組序列
- 文字 → Unicode 跳脫序列 (
Emoji 編碼
- 開啟 Emoji 編碼工具
- 輸入 Emoji
- 查看完整編碼資訊:
- 碼位序列
- 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#字符编码#码点