本文 AI 產出,尚未審核

JavaScript 字串操作 – 字元編碼與字串長度


簡介

在 JavaScript 中,字串是最常見的資料型別之一,幾乎所有的前端與後端程式都會與文字打交道。了解字元編碼的原理,才能正確判斷字串長度、切割子字串、或是安全地處理使用者輸入。
如果只依賴 str.length,在面對 Emoji、中文、或是其他 Unicode 之外的字符時,往往會得到錯誤的結果,進而導致 UI 破版、資料庫儲存錯誤,甚至安全漏洞。

本篇文章將從 UTF‑16 編碼碼點 (code point)代理配對 (surrogate pair) 等核心概念說起,說明 JavaScript 為何會出現「字串長度不等於實際字元數」的情況,並提供 實用的程式碼範例常見陷阱最佳實踐,讓你在日常開發中能夠正確且有效率地操作字串。


核心概念

1. JavaScript 內部的字串儲存 – UTF‑16

JavaScript 的字串是以 UTF‑16 為底層編碼方式儲存的。每個 16 位元 (2 個位元組) 的單位稱為 code unit,而一個完整的 Unicode 字元 (code point) 可能由 一個或兩個 code unit 組成。

範例 Unicode 編碼點 UTF‑16 表示 需要的 code unit 數量
A U+0041 0x0041 1
U+4E2D 0x4E2D 1
😀 U+1F600 0xD83D 0xDE00 2 (代理配對)

重點:只有基本多語言平面 (BMP) 內的字符 (U+0000 ~ U+FFFF) 佔用一個 code unit;超出 BMP 的字符 (如 Emoji、某些古文字) 會以 代理配對 形式佔用 兩個 code unit。

2. String.length 與實際字元數

String.length 直接回傳 code unit 的數量,因此在含有代理配對的字串上會得到比實際字元多的值。

const str = 'Hello😀世界';
console.log(str.length); // 10
// 解析:H e l l o (5) + 😀 (2) + 世 界 (2) = 9? 其實是 10,因為「😀」佔兩個 code unit

如果要得到真正的字元數(即 Unicode code point 數),可以使用以下方法:

// 方法一:展開運算子 + 陣列長度
const realLength = [...'Hello😀世界'].length; // 9

// 方法二:Array.from
const realLength2 = Array.from('Hello😀世界').length; // 9

技巧[...str]Array.from(str) 會自動依 UTF‑16 代理配對 把字串切成真正的「字元」(code point)。

3. 正確切割與遍歷字串

3.1 使用 for...of

for...of 迭代時會以 字元 (code point) 為單位,而非單純的 code unit。

for (const ch of '🦄✨') {
  console.log(ch);
}
// 輸出:
// 🦄
// ✨

3.2 String.prototype.slice 與代理配對

slice 接收的參數仍是 code unit 索引,若不小心在代理配對的中間切割,會產生 半個字符,導致顯示異常。

const emoji = '👍';
console.log(emoji.slice(0, 1)); // � (破字)

解法:先把字串展開成陣列,再使用 slice,最後再 join 回字串。

function safeSlice(str, start, end) {
  return [...str].slice(start, end).join('');
}
console.log(safeSlice('👍🏻', 0, 1)); // 👍🏻

4. TextEncoder / TextDecoder – 轉換成 UTF‑8

在與後端或檔案系統交互時,往往需要 UTF‑8 編碼的位元組。TextEncoder 會把字串轉成 Uint8Array,而 TextDecoder 則相反。

const encoder = new TextEncoder(); // 預設使用 UTF‑8
const decoder = new TextDecoder('utf-8');

const utf8Bytes = encoder.encode('Hello 🌍');
console.log(utf8Bytes); // Uint8Array([...])

const backToString = decoder.decode(utf8Bytes);
console.log(backToString); // "Hello 🌍"

5. 正規表達式與 Unicode flag (u)

ECMAScript 2015 引入 u flag,使正則表達式支援 Unicode code point,避免因代理配對產生錯誤匹配。

const regex = /\p{Emoji}/u; // 匹配任意 Emoji
console.log(regex.test('😀')); // true
console.log(regex.test('A')); // false

程式碼範例

以下範例展示在日常開發中會用到的 字元編碼與長度 操作技巧,皆附有說明註解。

範例 1 – 計算真實字元長度

/**
 * 回傳字串的 Unicode 字元數(不受代理配對影響)
 * @param {string} str
 * @returns {number}
 */
function getUnicodeLength(str) {
  // 展開運算子會自動把代理配對視為單一字元
  return [...str].length;
}

console.log(getUnicodeLength('JavaScript')); // 10
console.log(getUnicodeLength('🌟✨🚀')); // 3
console.log(getUnicodeLength('漢字')); // 2

範例 2 – 安全切割含 Emoji 的字串

/**
 * 依 Unicode 字元切割字串
 * @param {string} str 原始字串
 * @param {number} start 起始索引(含)
 * @param {number} end 結束索引(不含)
 * @returns {string}
 */
function safeSubstring(str, start, end) {
  // 先展開成陣列,再切割,最後合併回字串
  return [...str].slice(start, end).join('');
}

const text = '🚀 Launch 🚀';
console.log(safeSubstring(text, 0, 2)); // "🚀 "
console.log(safeSubstring(text, 2, 8)); // "Launch"

範例 3 – 逐字元遍歷並統計 Emoji 數量

function countEmojis(str) {
  let count = 0;
  // 使用 for...of 逐個 Unicode 字元遍歷
  for (const ch of str) {
    // 判斷是否屬於 Emoji 範圍(簡易寫法)
    if (/[\u{1F300}-\u{1FAFF}]/u.test(ch)) {
      count++;
    }
  }
  return count;
}

console.log(countEmojis('Hello 😊! 🎉')); // 2

範例 4 – 使用 TextEncoder 計算 UTF‑8 位元組長度

function utf8ByteLength(str) {
  const encoder = new TextEncoder(); // UTF‑8
  return encoder.encode(str).length;
}

console.log(utf8ByteLength('A')); // 1
console.log(utf8ByteLength('中')); // 3
console.log(utf8ByteLength('😀')); // 4

範例 5 – 正則表達式 u flag 解析 Unicode

// 捕捉所有非 BMP(即需要代理配對的字符)
const nonBMP = /[\u{10000}-\u{10FFFF}]/gu;

const mixed = 'a😀b𠮷c';
console.log(mixed.match(nonBMP)); // ["😀", "𠮷"]

常見陷阱與最佳實踐

陷阱 說明 解決方案
直接使用 str.length 在含有 Emoji、中文或其他非 BMP 字元時會得到錯誤的長度 使用 [...str].lengthArray.from(str).length
slicesubstring 切割代理配對 可能產生半個字符,導致顯示「�」或亂碼 先展開字串為陣列,再切割,最後 join
正則表達式未加 u flag \w. 等會把代理配對視為兩個字元,匹配結果不正確 加上 u flag,或使用 Unicode Property Escape (\p{...})
字串與位元組混用 把 UTF‑16 長度當作 UTF‑8 位元組長度,會在傳輸或儲存時出錯 使用 TextEncoder/TextDecoder 轉換編碼
字串比較時忽略正規化 同一視覺字元可能有不同的 Unicode 正規化形式 (NFCNFD),比較結果會不相等 使用 String.prototype.normalize() 統一正規化形式

最佳實踐

  1. 統一編碼:在 API、資料庫、檔案 I/O 前,確保所有字串皆已正規化為 NFC(最常用)。
  2. 使用 Unicode-aware 方法for...ofArray.from[...str]、正則 u flag。
  3. 避免硬編碼索引:若需切割或取子字串,儘量使用 字元索引(透過展開陣列)而非 length
  4. 測試多語系與 Emoji:在單元測試中加入包含代理配對的字串,以捕捉潛在錯誤。

實際應用場景

場景 需求 相關技巧
使用者輸入驗證 限制字數、檢查是否包含 Emoji 使用 [...input].length 取得正確字數;正則 u flag 檢測 Emoji
UI 顯示截斷 顯示省略號 ,不讓文字在中間斷開 safeSubstring(str, 0, maxChars) + (str.length > maxChars ? '…' : '')
搜尋與排序 依 Unicode 字元排序,確保一致性 localeCompare 搭配 normalize()
多語系文件上傳 計算檔名長度、避免超過系統限制 utf8ByteLength(filename) 取得實際傳輸位元組長度
即時聊天系統 正確統計訊息長度、限制 Emoji 數量 countEmojis(message)getUnicodeLength(message)

總結

  • JavaScript 以 UTF‑16 為底層編碼,String.length 只代表 code unit 數,在遇到代理配對時會產生誤差。
  • 想要得到真實的字元數安全切割遍歷字串,必須使用 Unicode‑aware 的方法:[...str]Array.fromfor...of、正則 u flag 等。
  • 在需要 位元組長度(如網路傳輸、檔案儲存)時,應以 TextEncoder / TextDecoder 轉成 UTF‑8,避免直接使用 length
  • 常見陷阱包括 代理配對切割、正則未加 u、忽略字串正規化,只要遵循 「先展開再操作」 的原則,就能有效避免。

掌握上述概念與實作技巧,你就能在日常開發中正確、可靠地處理各種文字資料,從簡單的表單驗證到複雜的多語系系統,都能游刃有餘。祝你寫程式寫得 順手又順心