处理多语言文本时,一个最常见的需求就是“把一段文字拆成有意义的片段”。可能是按词拆,也可能是按句子拆,但大部分开发者第一反应是 split(' ') 或者用正则。这种方法在纯英文场景里勉强能跑,一旦遇到中文、日文这种没有空格的语言,或者碰上带肤色修饰符的 emoji 序列,结果就乱套了。从 2021 年的 Chrome 87 开始,浏览器和 Node.js 陆续支持了 Intl.Segmenter,这个 API 能根据语言的天然边界来分割文本——字素(grapheme)、单词、句子——完全符合 Unicode 规范。本文用两个需求明确、产出可以直接复用的案例,带你把它摸熟。
一、传统方法的局限
假设我们要做一个简单功能:计算一条用户评论的字数。朴素的写法是 str.length,但马上就会出问题。一个 emoji 比如 👨👩👧👦,length 会返回 11,而用户眼里它只是一个“符号”。另一个例子是 é 这种带重音的组合字符,str.length 是 2,但它是一个字母。
再换一种思路,用 [...str] 或者 Array.from(str) 做字素分割,这个确实比 .length 好得多,能把基本的多字节字符处理好。但它在处理复杂的 emoji 序列(比如 👨🏾🎤)或者泰文、阿拉伯文这类连字时,依然会拆出一些中间态的单元,不是用户感知的“一个字”。而且它没法按单词或句子分割。
Intl.Segmenter 就是专门解决这个问题的。它支持三种粒度:
- grapheme —— 字素簇,用户感知的最小字符单元。
- word —— 单词。
- sentence —— 句子。
更关键的是,它可以接收 locale 参数,根据不同语言的规则来判断边界。同一个字符串用中文 locale 和英文 locale 切成的“单词”可能完全不同。
二、API 基本用法
创建一个 Intl.Segmenter 实例,指定 locale 和粒度:
const segmenter = new Intl.Segmenter('zh-CN', { granularity: 'word' });
然后调用 .segment(text) 得到一个可迭代对象,每一项包含 segment(文本片段)、index、input 以及 isWordLike(仅 word 粒度下有这个属性,表示是不是真正的“词”而不是空格或标点)。
const text = "Hello, 世界!";
const segments = segmenter.segment(text);
for (const item of segments) {
console.log(item.segment, item.isWordLike);
}
// 输出:
// "Hello" true
// "," false
// " " false
// "世界" true
// "!" false
注意 isWordLike 区分了真正的词汇和标点、空格。这比正则按 b 拆分准确得多,因为中文的“世界”虽然没有空格分隔,但 segmenter 知道它是一个词。
三、实战案例一:精确的字数统计
社交平台的字数限制通常按“用户看到的字数”来计算。emoji 算一个字,中文一个字,英文一个单词可能算一个。如果把一个 emoji 算成 11 个字符,用户的输入会莫名其妙地被截断。我们基于 grapheme 粒度来实现统计:
function countGraphemes(text) {
if (!text) return 0;
const segmenter = new Intl.Segmenter('zh-CN', { granularity: 'grapheme' });
const segments = segmenter.segment(text);
// 展开成数组,因为迭代器没有直接的 length
return [...segments].length;
}
// 测试
console.log(countGraphemes('👨👩👧👦')); // 1
console.log(countGraphemes('é')); // 1
console.log(countGraphemes('hello')); // 5
console.log(countGraphemes('你好世界')); // 4
console.log(countGraphemes('👋🏼 hello')); // 7 (👋🏼算1个,空格算1个,hello算5个)
和传统的 str.length 或 [...str].length 对比一下:
const emoji = '👨👩👧👦';
console.log(emoji.length); // 11
console.log([...emoji].length); // 7
console.log(countGraphemes(emoji)); // 1
差别非常直观。segmenter 准确地把整个家庭 emoji 当作一个单位,因为它内部使用了 Unicode 的扩展字素簇算法,知道零宽连接符(ZWJ)连接的多个 emoji 应该算一个整体。
四、实战案例二:安全的字符串截断
把长文本截短加省略号,新手最容易写出的代码是 str.slice(0, maxLength) + '...'。这如果在 emoji 中间切一刀,就会产生一个乱码字符(比如只切掉了肤色修饰符的一半)。正确的做法是按字素边界截断,确保切点不会破坏一个完整的字形。
function truncateByGrapheme(text, maxCount) {
if (!text) return '';
const segmenter = new Intl.Segmenter('zh-CN', { granularity: 'grapheme' });
const segments = [...segmenter.segment(text)];
if (segments.length s.segment).join('') + '...';
}
// 演示
const longText = '👨👩👧👦 是一个大家庭,他们很开心。';
console.log(truncateByGrapheme(longText, 5));
// 输出: "👨👩👧👦 是一个大..."
如果不使用 segmenter,截取前 10 个字符,很可能把 emoji 肢解,产生一串问号或者带框的问号。segmenter 则保证每个片段完整。
五、word 粒度的妙用:给中文加自然换行
中文在网页里自动换行没问题,但有时候需要在前端做文本分析,比如提取关键词、分词。segmenter 的 word 粒度配合 locale 可以提供不错的基础分词能力,尽管比不上专业 NLP 工具,但胜在零依赖。
function extractWords(text, locale = 'zh-CN') {
const segmenter = new Intl.Segmenter(locale, { granularity: 'word' });
return [...segmenter.segment(text)]
.filter(item => item.isWordLike)
.map(item => item.segment);
}
console.log(extractWords('今天天气真好,适合写代码。'));
// ["今天", "天气", "真好", "适合", "写", "代码"]
对于英文,它就是自然的分词工具:
console.log(extractWords('Hello, how are you today?', 'en-US'));
// ["Hello", "how", "are", "you", "today"]
六、sentence 粒度:文本摘要前的段落拆分
在做自动摘要或长文分段时,需要先把文本拆成句子。segmenter 的 sentence 粒度能处理中英文的标点差异,比单纯用 split(/[.!?]/) 靠谱得多。
function splitSentences(text, locale = 'zh-CN') {
const segmenter = new Intl.Segmenter(locale, { granularity: 'sentence' });
return [...segmenter.segment(text)].map(s => s.segment.trim()).filter(Boolean);
}
const paragraph = '今天天气很好。下午我去了公园,玩得很开心!你觉得呢?';
console.log(splitSentences(paragraph));
// ["今天天气很好。", "下午我去了公园,玩得很开心!", "你觉得呢?"]
七、浏览器和 Node.js 支持度
Intl.Segmenter 属于 Intl 标准的一部分,目前的支持情况:
- Chrome 87+,Edge 87+
- Firefox 125+(2024年4月已支持)
- Safari 14.1+(2021年4月已支持)
- Node.js 16.0+(通过 V8 引擎)
全球覆盖率已经超过 95%,对主流项目几乎不需要考虑兼容补丁。对于极少数老浏览器,可以写一个简单的判断降级到 Array.from 或正则方案,只是分词能力弱一些。
八、值得注意的细节
在项目里实际用起来后,还有几个小地方要留心:
1. 迭代器不能重复使用。segmenter.segment() 返回的是迭代器,要多次使用需要重新调用或者转成数组。这点在数据量大时要注意性能,尽量只在必要时才展开成数组。
2. 不同 locale 的分词结果不同。比如“黑色”用 zh-CN 是一个词,用 en-US 可能就是两个字符。根据你的业务场景选择正确的 locale。
3. 性能表现。segmenter 的实现是 C++ 层的 Unicode 算法,处理几万字的长文本非常快,几乎不会成为前端性能瓶颈。
九、总结
Intl.Segmenter 的出现,让前端终于有了一个可靠的、开箱即用的文本分割方案。它把你从繁琐的正则和跨语言兼容中解放出来,用最自然的方式解决了 emoji、连字、分词等老大难问题。文中的字数统计和截断函数可以直接复制到你的项目里,替换掉那些潜在的 bug 源头。下一次遇到多语言文本处理时,不用再去 npm 上翻库,浏览器已经帮你准备好了。

