本文 AI 產出,尚未審核

JavaScript 字串操作:Unicode 與 Emoji 處理


簡介

在日常的 Web 開發或 Node.js 應用中,字串是最常見的資料類型之一。隨著行動裝置與社群平台的普及,使用者輸入的文字不再只局限於 ASCII 或單一語系,Unicode(統一碼)與 emoji(表情符號)已成為不可或缺的元素。

如果對 Unicode 的特性缺乏了解,往往會在字串長度計算、切割、正則表達式或儲存時遭遇意外錯誤,甚至導致 UI 顯示錯亂。本文將從基礎概念說起,示範在 JavaScript 中正確且安全地處理 Unicode 與 emoji,協助初學者與中階開發者寫出更健全的程式碼。


核心概念

1️⃣ 什麼是 Unicode?

Unicode 是一套為全球所有文字、符號、表情等分配唯一編號(code point)的標準。每個字符都有一個十六進位的碼點,例如:

字符 代表的 Unicode 碼點
A U+0041
U+4E2D
🌈 U+1F308

JavaScript 內部使用 UTF‑16 編碼,每個 16 位元(2 byte)單位稱為 code unit。對於大多數 BMP(Basic Multilingual Plane,U+0000~U+FFFF)內的字符,一個 code unit 即能完整表示;但對於超出 BMP 的字符(如大多數 emoji),會以 兩個 code unit(稱為 surrogate pair)儲存。

⚠️ 重點:在 JavaScript 中,String.length 回傳的是 code unit 數量,而非真正的字符(code point)數量。

2️⃣ 為什麼普通的字串操作會失效?

以下示例說明常見的錯誤情況:

const s = '🌈';        // U+1F308,使用 surrogate pair
console.log(s.length); // 2  ← 不是 1
console.log(s[0]);     // �  ← 只取到高位代理 (high surrogate)

如果直接使用 slicesubstringcharAt,很可能會切斷半個 surrogate pair,產生無效的 Unicode 序列,導致瀏覽器顯示「�」或拋出錯誤。

3️⃣ 正確取得 Unicode length(code point 數)

ES6 引入了 Array.from擴充運算子(spread operator),可將字串自動展開為「真實字符」的陣列,從而正確取得長度:

const emoji = '👩‍💻'; // 👩 + ZWJ + 💻,實際上是一個「複合 emoji」
const chars = Array.from(emoji); // → ['👩', '‍', '💻']
console.log(chars.length); // 3(每個 code point)

如果想要把 整個顯示單位(包括 ZWJ 連結的複合 emoji)視為一個字元,可使用 Intl.Segmenter(Node 12+、瀏覽器支援較新):

const seg = new Intl.Segmenter('en', { granularity: 'grapheme' });
const graphemes = [...seg.segment(emoji)];
console.log(graphemes.length); // 1,視為單一「圖素」(grapheme cluster)

4️⃣ 安全的字串切割與遍歷

下面提供三個實務範例,示範如何在保留 Unicode 正確性的前提下進行切割、遍歷與搜尋。

範例 1:安全切割(取前 n 個顯示單位)

/**
 * 取出字串的前 n 個 grapheme cluster(顯示單位)
 * @param {string} str 來源字串
 * @param {number} n   取前 n 個顯示單位
 * @returns {string}
 */
function sliceGrapheme(str, n) {
  const seg = new Intl.Segmenter('zh-Hant', { granularity: 'grapheme' });
  const iterator = seg.segment(str)[Symbol.iterator]();
  let result = '';
  let count = 0;
  for (let { value, done } = iterator.next(); !done && count < n; { value, done } = iterator.next()) {
    result += value;
    count++;
  }
  return result;
}

console.log(sliceGrapheme('Hello 🌏👩‍💻世界', 7)); // "Hello 🌏👩‍💻"

說明Intl.Segmenter 會把「👩‍💻」視為單一顯示單位,避免切斷半個 emoji。

範例 2:遍歷每個真實字符(code point)

const text = '𠜎abc🌟';
for (const ch of text) {
  console.log(ch, ch.codePointAt(0).toString(16));
}
// 輸出:
// 𠜎 20b9e
// a 61
// b 62
// c 63
// 🌟 1f31f

使用 for...of(或 Array.from)會自動以 code point 為單位迭代,對於 surrogate pair 不會產生半字的情況。

範例 3:正則表達式匹配 emoji

ES2022 引入了 Unicode property escapes\p{...}),可直接匹配 Emoji、中文、拉丁字母等屬性:

const msg = '今天心情很好 😊,一起加油! 🚀';
const emojiPattern = /\p{Emoji_Presentation}/gu; // 只抓出「呈現為 emoji」的字符
const emojis = msg.match(emojiPattern);
console.log(emojis); // ["😊", "🚀"]

注意:正則必須加上 u(Unicode)旗標,否則會把 surrogate pair 當成兩個獨立字符。

5️⃣ 字串與儲存(UTF‑8、UTF‑16)

在伺服器端(Node.js)或資料庫中,通常會使用 UTF‑8 編碼儲存文字。JavaScript 的 Buffer 物件可以在 UTF‑8 與 UTF‑16 之間轉換:

const str = '💡✨';
const utf8 = Buffer.from(str, 'utf8');   // 轉成 UTF‑8 位元組
console.log(utf8); // <Buffer f0 9f 92 a1 e2 9c a8>
const back = utf8.toString('utf8');
console.log(back === str); // true

確保在 API 交互檔案寫入 時使用正確的編碼,可避免「亂碼」或資料遺失。


常見陷阱與最佳實踐

陷阱 說明 解決方案
String.length 直接使用 只算 code unit,對 emoji 會得到 2 或更多 使用 Array.from(str).lengthIntl.Segmenter
正則未加 u flag 會把 surrogate pair 拆開,匹配失敗或產生錯誤 加上 /u,或使用 Unicode property escape
切割時使用 slicesubstring 可能切斷 surrogate pair,產生非法字符 使用 sliceGrapheme(基於 Intl.Segmenter
直接比較字串長度判斷「字數」 中文、emoji 會被計算成多個單位 先正規化為 grapheme,再比較
輸入/輸出編碼不一致 在 Node.js 與資料庫間傳遞時產生亂碼 明確指定編碼(utf8)並使用 Buffer 轉換

最佳實踐

  1. 總是使用 Unicode‑aware 方法for...ofArray.fromIntl.Segmenter
  2. 正則表達式加上 u flag,必要時使用 \p{...}
  3. 在 UI 中顯示 emoji 前,先正規化(NFC)以避免不同平台呈現差異。
  4. 儲存與傳輸時統一使用 UTF‑8,避免跨平台編碼衝突。
  5. 測試包含多語系與 emoji 的字串,尤其是長度、切割與搜尋的單元測試。

實際應用場景

場景 為何需要 Unicode/emoji 處理 可能的實作方式
社群平台貼文編輯器 使用者會貼上大量 emoji、ZWNJ/ZWJ 連結的複合表情 使用 Intl.Segmenter 計算貼文字數上限,確保不會因切斷半個 emoji 而產生錯誤
聊天機器人 (Chatbot) 需要辨識使用者輸入的情緒 emoji,做出回應 正則 \p{Emoji} 搭配 matchAll 取得所有表情,計算情緒分數
多語系搜尋引擎 文字搜尋需支援中文、日文、表情符號等 建立倒排索引時,使用 grapheme 單位切詞,避免把 emoji 與相鄰文字混在一起
檔案上傳與儲存 檔名可能包含 Unicode 字元或 emoji 在 Node.js 中使用 fs 時,以 UTF‑8 編碼寫入,並使用 path.normalize 處理路徑
國際化 UI (i18n) 不同語系的文字長度差異大,emoji 亦佔位 使用 Intl.Segmenter 動態計算字串長度,調整 UI 元素寬度或字體大小

總結

Unicode 與 emoji 已成為現代 Web 與應用程式不可或缺的文字元素。

  • JavaScript 內部使用 UTF‑16,String.length 並不等於實際字符數
  • 透過 Array.fromfor...ofIntl.Segmenter,我們可以安全地遍歷、切割與計算字串長度。
  • 正則表達式必須加上 u flag,並善用 Unicode property escapes 來匹配 Emoji。
  • 在儲存、傳輸或跨平台顯示時,統一使用 UTF‑8 編碼,並做好正規化。

掌握這些概念與實作技巧後,你就能在開發聊天應用、社群平台、國際化網站等場景時,避免因 Unicode 處理不當而產生的「亂碼」或「字元斷裂」問題,寫出更健全、使用者友善的程式碼。祝你在 JavaScript 的字串世界裡玩得開心、寫得順利!