先记住这个答案
Intl.Segmenter 是按语言规则切分文本的 API。构造时通过 granularity 选粒度:grapheme 切成用户看到的字符,word 切成单词,sentence 切成句子。调用 segment(text) 返回一个可迭代对象,每一项有 segment(这一段的文本)、index(在原字符串里的起始位置),word 模式下还有 isWordLike,用来区分词和标点空格。它解决了两个老问题:split(' ') 切不了没有空格的中文和日文,str.length 数的是 UTF-16 单元,一个带肤色的 emoji 会被算成 4。
grapheme是用户眼里的一个字符,emoji 不会被拆开word能给中文、日文分词,isWordLike过滤标点index是 UTF-16 偏移,不是第几个字符- 返回的是可迭代对象,要数组就用展开运算符
字数统计和安全截断:grapheme
评论框限制 100 个字,用 length 判断,用户输入几个 emoji 就被多算了。截断昵称时用 slice,还可能把一个 emoji 从中间切开,显示成乱码。
const text = '点赞👍🏻' // 竖大拇指 + 肤色修饰符
const seg = new Intl.Segmenter('zh-CN', { granularity: 'grapheme' })
const graphemes = [...seg.segment(text)]
console.log(text.length) // UTF-16 单元数
console.log([...text].length) // 码点数
console.log(graphemes.length) // 用户看到的字符数
// 安全截断:按字素取前 3 个,不会切坏 emoji
console.log(graphemes.slice(0, 3).map((s) => s.segment).join('') === text)查看输出与解释
6
4
3
true👍🏻 是两个码点拼成的:竖大拇指和一个肤色修饰符,每个码点在 UTF-16 里又要两个单元,所以 length 是 4。展开运算符 [...text] 按码点拆,能解决「一个码点两个单元」的问题,但拆不了多码点组合,还是算成 2。只有 grapheme 粒度能算出用户眼里的 1 个字符。
如果用 text.slice(0, 3) 截取,会切在大拇指那个码点的中间,页面上显示成一个问号方块。按字素截断就没有这个问题。家庭 emoji、国旗、带声调符号的字母也是同样的道理,它们都由多个码点组成。
中文分词和分句:word 和 sentence
英文按空格就能大致分词,中文不行。word 粒度会用内置的词典找词的边界。
const words = new Intl.Segmenter('zh-CN', { granularity: 'word' })
const list = [...words.segment('我喜欢写JavaScript。')]
.filter((s) => s.isWordLike) // 去掉句号这类非词片段
.map((s) => `${s.segment}@${s.index}`)
console.log(list.join(' '))
const sentences = new Intl.Segmenter('zh-CN', { granularity: 'sentence' })
const parts = [...sentences.segment('今天下雨了。你带伞了吗?带了!')]
console.log(parts.map((s) => s.segment).join(' | '))查看输出与解释
我@0 喜欢@1 写@3 JavaScript@4
今天下雨了。 | 你带伞了吗? | 带了!「喜欢」被识别成一个词,从下标 1 开始。句号那一段的 isWordLike 是 false,被过滤掉了。用这个方法可以做中文的字数统计、双击选中一个词、按词高亮搜索关键字。
分句按句号、问号、感叹号切。但规则是通用的,英文里「Mr. Smith left.」会被切成「Mr.」和后面两段,因为它把缩写后的点也当成了句末。对准确性要求高的场景,比如机器翻译前的分句,还要额外处理。
index不是第几个字符index是这一段在原字符串里的 UTF-16 偏移,可以直接拿去slice。但它不等于「第几个可见字符」,前面有 emoji 的话,index会比字素序号大。要显示「第 N 个字」时,得用grapheme粒度另外数。
容易答错的地方
- 以为返回值是数组
segment()返回的是一个可迭代的Segments对象,没有map、filter,要先用[...segments]或Array.from()转成数组。它还有一个containing(index)方法,可以直接查某个位置属于哪一段,做「双击选词」很方便。- 用
grapheme的结果算大模型 token - 字素是用户看到的字符,大模型的 token 是按它自己的分词表切的,两者没有对应关系。算 token 数、估算费用,必须用对应模型的 tokenizer。
面试官还会怎么问?
不用 Intl.Segmenter,怎么数 emoji?
[...str].length 能处理大部分单码点 emoji,但处理不了肤色、家庭、国旗这类多码点组合。以前的做法是引入 grapheme-splitter 这类库,现在直接用 Intl.Segmenter 就行,主流浏览器都支持,Firefox 是最后一个跟进的。
isWordLike 为什么只在 word 粒度有?
它的作用是区分词语和空格、标点。grapheme 和 sentence 粒度的任务不是判断「像不像词」,所以返回的片段里没有这个字段。
不同浏览器分词结果会不一样吗?
会。分词依赖 ICU 的词典和规则,浏览器升级时可能更新。关键业务不要依赖某个具体的切分结果,比如不要把分词结果当成数据库索引的唯一依据,测试时也不要写死某个词一定被切成几段。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。