JavaScript emoji 字數怎麼算?Intl.Segmenter 計數與安全截斷
本文目錄
表單只輸入一個家庭 emoji,text.length 卻是 11,直接 slice(0, 1) 還可能留下半個代理對。JavaScript 的 length 計算 UTF-16 code units;畫面上的字數與截斷,應先按 grapheme cluster(字素叢集)定義需求,再使用 Intl.Segmenter。
MDN String.length說明 code unit 與 Unicode 字元的差異。[...text] 按 code point 展開,能保留單一 supplementary code point,但家庭、膚色及國旗等組合仍需字素分段。
先用同一組資料看出差異
以下是獨立 .mjs 程式,可用 Node.js 執行:
import assert from 'node:assert/strict';
const segmenter = new Intl.Segmenter('zh-TW', { granularity: 'grapheme' });const graphemes = text => Array.from(segmenter.segment(text), item => item.segment);const cases = [ ['', 0, 0, 0], ['台', 1, 1, 1], ['😀', 2, 1, 1], ['é', 2, 2, 1], ['👍🏽', 4, 2, 1], ['🇹🇼', 4, 2, 1], ['👨👩👧👦', 11, 7, 1],];for (const [text, units, points, clusters] of cases) { assert.equal(text.length, units); assert.equal([...text].length, points); assert.equal(graphemes(text).length, clusters);}console.log('三種計數:PASS');這是字元分段驗證;實際字型可能把同一字素畫成不同圖像。Intl.Segmenter 文件區分 grapheme、word 與 sentence,用於畫面字數時明確選 grapheme。
截斷時保留整個字素
下例將上限定義為保留的原文字素數,回傳值不附省略號。若產品要加 …,應另訂它是否占用上限,避免前端顯示與後端驗證使用不同政策。
import assert from 'node:assert/strict';
function truncateGraphemes(text, max) { if (typeof text !== 'string') throw new TypeError('text 必須是字串'); if (!Number.isSafeInteger(max) || max < 0) { throw new RangeError('max 必須是非負安全整數'); } if (typeof Intl.Segmenter !== 'function') { throw new Error('此環境需要支援 Intl.Segmenter'); } if (max === 0) return ''; const segmenter = new Intl.Segmenter('zh-TW', { granularity: 'grapheme' }); let result = ''; let count = 0; for (const { segment } of segmenter.segment(text)) { result += segment; count += 1; if (count === max) break; } return result;}
assert.equal(truncateGraphemes('A👨👩👧👦B', 2), 'A👨👩👧👦');assert.equal(truncateGraphemes('é台', 1), 'é');assert.equal(truncateGraphemes('🇹🇼👍🏽', 1), '🇹🇼');assert.equal(truncateGraphemes('abc', 10), 'abc');assert.equal(truncateGraphemes('', 2), '');assert.equal(truncateGraphemes('abc', 0), '');for (const max of [-1, 1.5, NaN, Infinity, '2']) { assert.throws(() => truncateGraphemes('abc', max), RangeError);}assert.throws(() => truncateGraphemes(null, 2), TypeError);console.log('字素截斷:PASS');本次維護於 2026-11-07 在 macOS、Node.js v24.13.0 執行兩段程式,斷言通過;未做瀏覽器表單整合或字型顯示測試。部署時以 feature detection 確認目標環境,必要時採已驗證的分段實作,並以相同資料重跑前後端測試。
畫面字數與儲存限制各自驗證
如果 API 限制 UTF-8 bytes,字素計數只能提供畫面提示;後端仍按自己的編碼與上限驗證。若欄位約定 code points 或 UTF-16 units,就保留該契約並向使用者說清楚,避免前端放行後遭後端拒絕。字素數也無法直接推算模型 tokens。
需要 JSON 往返時,可搭配 JSON 欄位與 undefined 契約確認送出的內容。先選定計數單位,再讓提示、截斷與儲存端使用可核對的規則。
參考資料: