686 字
3 分鐘

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 契約確認送出的內容。先選定計數單位,再讓提示、截斷與儲存端使用可核對的規則。

參考資料:

MDN:String.length

MDN:Intl.Segmenter

JavaScript emoji 字數怎麼算?Intl.Segmenter 計數與安全截斷
https://laplusda.com/posts/javascript-string-length-grapheme-segmenter/
作者
Zero
發佈於
2026-11-07
許可協議
CC BY-NC-SA 4.0