文字與寫作 指南
Emoji 為什麼看起來1個,程式卻可能算2個以上?
「字元」在 Unicode 世界至少有三種常見意思:code unit、code point、grapheme cluster。Emoji 正好把差異全部放大。
更新日期:
例如:
🦊
對人來說:
一個狐狸 emoji。
但 JavaScript:
"🦊".length
得到的不是使用者直覺中的「1」。
原因是 JavaScript String 的 length 計算:
UTF-16 code units。
第一層:Code Unit
JavaScript 內部使用 UTF-16。
BMP 之外的 code point 需要 surrogate pair:
兩個 UTF-16 code units。
所以某些 emoji:
看起來1個,
.length卻是2。
第二層:Code Point
如果你用:
[..."🦊"].length
會比較接近 Unicode code points。
但這仍然不一定等於:
人類看起來的字元。
第三層:Grapheme Cluster
Unicode 使用:
grapheme cluster
來近似「使用者感知的一個字元」。
例如某些:
- family emoji
- skin tone emoji
- flags
- combining accent
可能由多個 code points 組成,但使用者仍看成一個。
Unicode UAX #29 明確指出:
使用者感知字元不一定只是一個 code point。
Family Emoji 為什麼特別長?
有些 family emoji 是多個人物 emoji 加:
Zero Width Joiner(ZWJ)
連起來。
畫面:
一個家庭圖示。
底層:
多個 Unicode 元件。
因此 character counter 到底顯示什麼,取決於它計的是:
- UTF-16 code units
- code points
- grapheme clusters
瀏覽器有 Intl.Segmenter
現代 JavaScript 可使用:
new Intl.Segmenter(locale, {
granularity: "grapheme"
})
依 grapheme cluster 分段。
這比 .length 更接近:
使用者認為幾個字元。
但舊瀏覽器支援度與平台自身算法仍可能不同。
FunnyTools Character Counter 要怎麼看?
目前工具公開提醒:
許多 emoji 可能顯示為2個以上字元,UTF-8 bytes也通常高於英文字母。
因此它適合:
預先發現「emoji 可能讓平台限制和直覺不同」。
但若某平台規則寫:
Unicode scalar values
graphemes
UTF-16 units
就必須依平台規格。
Flags
旗幟 emoji 也很經典。
畫面:
🇯🇵
你看成:
一面旗。
底層可能由區域指示符 code points 組成。
所以:
「看起來一個圖示」不能直接推導程式 count。
Combining Accent
某些文字:
é
可能:
- 單一 precomposed code point
也可能:
e- combining acute accent
畫面相同。
底層 representation 不一定相同。
Unicode normalization 就是另一層需要注意的問題。
什麼時候需要 Grapheme Count?
比較適合:
- UI 顯示限制
- 編輯器游標
- backspace
- 使用者可見長度
- 暱稱限制
但 API/database 若限制 bytes:
Grapheme count 完全不夠。
一張表搞懂
| 觀念 | 問題 |
|---|---|
| UTF-16 code unit | JavaScript .length 常看到什麼 |
| Code point | Unicode 編碼點有幾個 |
| Grapheme cluster | 使用者看起來幾個字元 |
| UTF-8 byte | 傳輸/儲存需要多少 bytes |
建議
當需求文件只寫:
maximum 20 characters
但允許 emoji:
請要求工程團隊定義「character」到底是哪一種。
這比事後處理 bug 更有效。
相關工具/指南
以使用者感知測試介面
介面顯示「還剩幾個字」時,應先定義這個數字服務誰。若是使用者輸入限制,半個家庭 emoji 或被拆開的變音符號都不應被接受;若是後端欄位限制,則要另做 bytes 和 code unit 檢查。測試人員可在游標前後刪除一個 grapheme,觀察是否整組消失,再檢查複製、貼上、退格鍵和表單送出。這些操作比只在程式 console 印出 length 更接近真實使用,也能發現視覺內容和儲存內容不一致的問題。
產品限制應該選哪種單位
若限制面向使用者,例如顯示名稱、貼文標題或留言欄,通常應先討論 grapheme cluster,也就是使用者感覺的一個字。這能避免家庭 emoji、膚色修飾、旗幟和 ZWJ 組合被拆開後只剩半個圖案。若限制面向資料庫或傳輸,則還要另外檢查 code point、UTF-16 code unit、UTF-8 bytes 與欄位大小,因為這些是工程實作單位,不等於畫面上的字數。
測試時不要只放一個笑臉。至少加入單一 BMP 字元、需要 surrogate pair 的 emoji、帶膚色的 emoji、旗幟、家庭或職業 ZWJ 序列,以及文字加變音符號。逐一記錄 JavaScript length、Array.from 長度、使用者感知數量與實際 bytes。規格若沒有說明 character 的定義,就應先補上定義和測試案例,再承諾上限。
截斷文字時要避免半個 emoji
顯示預覽或限制輸入長度時,不能只用字串索引切到指定位置,因為索引可能落在 surrogate pair、組合符號或 ZWJ 序列中。較安全的做法是先以 grapheme cluster 分段,再以完整片段累加到上限;若環境不支援 Intl.Segmenter,應選擇明確的 fallback,並把限制標示成估算值,而不是假裝每個畫面字元都能用 length 表示。
驗收時要測試剛好等於上限、超過一個 cluster、連續多個組合 emoji、游標刪除以及貼上超長內容。除了畫面,也要檢查送出的 JSON、資料庫欄位和錯誤訊息,確保前端截斷後沒有留下孤立的 variation selector 或不可見連接符。使用者看到的是完整圖形,工程端則應記錄採用的計數單位,兩者都要能被重現。
常見介面案例
留言欄、顯示名稱和社群貼文通常應以使用者感知的 grapheme 作為主要提示,並在送出前完整拒絕超限輸入;資料庫或 API 若另有限制,則應把 bytes 或 code units 的錯誤清楚分開。搜尋框若只限制畫面寬度,也不一定需要截斷文字,可以改用 CSS 省略並保留完整值,避免把顯示需求誤套成資料截斷需求。
對旗幟、膚色、家庭和職業 emoji,要驗證複製貼上後仍是同一個 cluster,並測試不同瀏覽器對 Intl.Segmenter 的支援差異。若 fallback 與主流程結果不同,應在規格中寫明取捨並加入回歸案例。這些案例能讓產品、設計和工程團隊使用同一套語言討論「一個字」,而不是各自引用不同的 length 數字。