跳至內容
返回部落格

"在 JavaScript 中將 Emoji 轉換為 Unicode"

Leibniz Li

@leibnizli
+

Emoji 不只是簡單字元,許多位於 Unicode 輔助平面。理解 JavaScript 如何處理它們,對字串長度、資料庫儲存和跨平台顯示尤其重要。

問題:為什麼 .length 會誤導你?

JavaScript 字串採用 UTF-16。A、☃(雪人)等字元位於基本多文種平面(BMP),占一個 16 位元碼元。😀 等 Emoji 位於輔助平面,用兩個 16 位元碼元組成的代理對表示。

'A'.length;    // 1 (BMP)
'☃'.length;    // 1 (BMP)
'😀'.length;    // 2 (Supplementary Plane - Surrogate Pair)
'👨‍👩‍👧‍👦'.length; // 11 (Wait, what? See "ZWJ" below)

方案一:String.prototype.codePointAt()

ES6 之前使用 charCodeAt(),它只回傳代理對的前半部分。現代 JavaScript 的 codePointAt() 可以正確取得完整的 Unicode 碼點。

const emoji = '😀';

// ❌ The old way (Incorrect for emojis)
console.log(emoji.charCodeAt(0)); // 55357 (Surrogate lead)

// ✅ The modern way
console.log(emoji.codePointAt(0)); // 128512
console.log(emoji.codePointAt(0).toString(16)); // "1f600"

方案二:String.fromCodePoint()

用靜態方法 String.fromCodePoint() 將 Unicode 數值還原為可見 Emoji。

// Decimal
String.fromCodePoint(128512); // "😀"

// Hexadecimal
String.fromCodePoint(0x1F600); // "😀"

// Multiple points
String.fromCodePoint(0x1F1FA, 0x1F1F8); // "🇺🇸"

批次轉換多個字元

包含多種 Emoji 和字元的字串可以一次轉換。展開運算子 [...] 按 Unicode 碼點迭代,不會拆開代理對。

1. 字串轉數值(編碼)

使用 .map() 取得十進位或十六進位的 Unicode 數值陣列:

const text = "Hi 😀 🚀";

// Convert to Decimal numbers
const decimals = [...text].map(char => char.codePointAt(0));
console.log(decimals); 
// [72, 105, 32, 128512, 32, 128640]

// Convert to Hexadecimal strings (common for CSS/JS escape)
const hexCodes = [...text].map(char => `0x${char.codePointAt(0).toString(16)}`);
console.log(hexCodes);
// ["0x48", "0x69", "0x20", "0x1f600", "0x20", "0x1f680"]

2. 數值轉字串(解碼)

將數值還原為可讀字串,使用 String.fromCodePoint 配合展開語法 ...

const myNumbers = [128512, 128640, 9731];

// This "spreads" the array items as individual arguments
const result = String.fromCodePoint(...myNumbers);
console.log(result); // "😀🚀☃"

3. 使用簡單迴圈

如果不熟悉 .map(),也可以用 for...of 迴圈:

const input = "🍎🍊";
for (let char of input) {
  let hex = char.codePointAt(0).toString(16);
  console.log(`Character: ${char} -> Unicode: U+${hex.toUpperCase()}`);
}
// Character: 🍎 -> Unicode: U+1F34E
// Character: 🍊 -> Unicode: U+1F34A

複雜 Emoji:零寬連接符(ZWJ)

一些 Emoji 看似單一字元,實際由多個 Unicode 碼點組成,以**零寬連接符(ZWJ,\u200D)**相連。

例如家庭 Emoji 👨‍👩‍👧‍👦 由以下序列組成: 男人 + ZWJ + 女人 + ZWJ + 女孩 + ZWJ + 男孩

使用展開運算子 [...]Array.from() 可以正確遍歷其碼點,但不會把整個家庭序列當成一個顯示字元:

const complexEmoji = '👨‍👩‍👧‍👦';

// ❌ Standard split (breaks the emoji)
console.log(complexEmoji.split('')); 
// ["\ud83d", "\udc68", "‍", "\ud83d", ...]

// ✅ Unicode-aware iteration
const points = [...complexEmoji];
console.log(points); 
// ["👨", "‍", "👩", "‍", "👧", "‍", "👦"]

// 1. Get code points as Hexadecimal (commonly used in docs)
const hexCodes = [...complexEmoji].map(c => `0x${c.codePointAt(0).toString(16)}`);
console.log(hexCodes);
// ["0x1f468", "0x200d", "0x1f469", "0x200d", "0x1f467", "0x200d", "0x1f466"]

// 2. Get code points as Decimal (Non-Hexadecimal)
const decimalCodes = [...complexEmoji].map(c => c.codePointAt(0));
console.log(decimalCodes);
// [128104, 8205, 128105, 8205, 128103, 8205, 128102]

// 3. Convert Decimal back to Emoji
console.log(String.fromCodePoint(...decimalCodes)); 
// "👨‍👩‍👧‍👦"

// 4. Convert Hex strings back to Emoji
const emojiFromHex = String.fromCodePoint(...hexCodes);
console.log(emojiFromHex); 
// "👨‍👩‍👧‍👦"

實際用途

  1. **資料庫儲存:**確保 MySQL 等資料庫使用 utf8mb4 儲存這些四位元組字元。
  2. **輸入限制:**限制個人簡介或推文長度時,[...str].length 可按碼點計數,而 str.length 按碼元計數。ZWJ 組合仍含多個碼點,不等同於使用者看到的字元數。
  3. **自訂文字算繪:**適用於 Canvas 遊戲或高效能介面元件。

需要互動工具?

經常進行這些轉換,可以試試線上 Unicode 與 Emoji 轉換器,即時雙向轉換文字、十六進位、CSS 與 JS 跳脫序列。

試用線上 Unicode 轉換器 →