intl.segmenter 不是语义分词器,而是基于 unicode uax#29 的边界检测 api;中文 word 级切分弱因规范限制,仅 ja-jp/zh-cn sentence 或 en-us word 可靠;需用原始索引高亮、统一 locale 预处理、显式兼容性检测。

Intl.Segmenter 不能“语义分词”,它只按 Unicode 标准做边界划分;但它是目前唯一能跨语言、免依赖、原生支持词级/句级索引定位的浏览器 API —— 关键在于用对 locale 和 granularity,并严格处理原始偏移量。
为什么 new Intl.Segmenter('zh-CN', { granularity: 'word' }) 在中文里几乎不切词
这不是 bug,是规范行为。Unicode UAX #29 对中文的 word boundary 定义极弱,主流浏览器(Chrome/Firefox)在 zh-CN 下基本只在标点、数字/字母混排处切分,多数汉字序列仍被当作单个 segment。Safari 更激进,可能直接退化为 grapheme 级别。
真正可用的组合只有:
-
ja-JP+{ granularity: 'word' }:能区分「食べる」「昨日」等黏着结构,切出助词与词干 -
zh-CN或ja-JP+{ granularity: 'sentence' }:可靠识别「。」「!」「?」及英文句点后的断句,中日英混排也稳 -
en-US+{ granularity: 'word' }:对标点+空格敏感,比.split(/\s+/)更准(保留连字符、撇号等)
搜索高亮必须基于原始字符串索引,不是词元数组下标
常见错误是拿到 segments[2].segment === '支付' 就以为原文位置是第 2 个词,然后用 text.replace(/支付/, '<mark>支付</mark>') —— 这会破坏后续所有 index 偏移,导致错位甚至死循环。
正确做法是遍历 segmenter.segment(text) 得到每个 { segment, index, input },再用 String.prototype.slice() 截取并包裹:
const seg = new Intl.Segmenter('ja-JP', { granularity: 'word' });
const segments = Array.from(seg.segment('東京都で食べる'));
// → [{ segment: '東京都', index: 0 }, { segment: 'で', index: 3 }, { segment: '食べる', index: 5 }]
// 搜索 '食べる' 时,用 segments.find(s => s.segment === '食べる')
// 再通过 s.index 和 s.segment.length 算出 [5, 5 + '食べる'.length) 区间
// 最后 text.slice(5, 5 + '食べる'.length) → 包裹 <mark></mark>
多语言混排搜索需统一预处理,且不能改原始文本
用户搜“Hello世界”,关键词含中英;若分别对原文和关键词用不同 locale 分词,结果无法对齐。必须选定一个主导 locale(如页面主语言),对二者都用同一 Intl.Segmenter 实例处理。
还要注意大小写和富文本:
- 大小写不敏感搜索:先将原文和关键词都转小写,但保存原始
segment和index用于最终高亮 - HTML 内容搜索:只对
textContent分词,匹配后用Range或手动解析标签位置映射回 DOM,避免把<span></span>当成可切分内容 - 绝不能在分词前用
replace()修改原文(比如去空格、过滤标签),否则index全乱
兼容性与降级必须显式检查,不能靠 try/catch 默默 fallback
typeof Intl.Segmenter !== 'function' 在 Edge 18 及更早、Safari 14.0 及更早版本为 undefined;传入非法 locale(如 'zh' 而非 'zh-CN')会导致静默降级为 en-US 行为,中文变空格切分 —— 这种错误很难被发现。
安全做法是:
- 创建实例后立刻调用
segmenter.resolvedOptions(),检查locale是否是你传入的值,否则报错或拒绝使用 - 对
granularity: 'word'做运行时探测:用已知日文词(如'食べる')测试是否被切成两段,否则 fallback 到sentence或正则粗切 - 服务端渲染(SSR)环境需确认 Node.js 版本 ≥ 18.17,且启用
--icu-data-dir参数,否则ja-JP词切分失效
最易被忽略的一点:segment() 返回的是迭代器,不是数组。想多次遍历必须重新调用 segmenter.segment(text),不能缓存迭代器对象——它是一次性的。










