
本文解释为何 'トゲ'.indexOf('ケ') 返回 1 且 'トゲ'.charAt(1) 返回 'ケ'——根本原因在于 Unicode 编码中半宽片假名“ゲ”实际由两个独立码点(半宽カ + 半宽浊点)组成,而非单一字符。
本文解释为何 `'トゲ'.indexof('ケ')` 返回 1 且 `'トゲ'.charat(1)` 返回 `'ケ'`——根本原因在于 unicode 编码中半宽片假名“ゲ”实际由两个独立码点(半宽カ + 半宽浊点)组成,而非单一字符。
在 JavaScript 字符串中,所有操作(如 indexOf、charAt、length、split('') 等)均基于 UTF-16 码元(code units) 进行处理,而非用户感知的“视觉字形”或“Unicode 抽象字符”。而半宽片假名「ゲ」在 Unicode 中并不存在独立码点;它必须通过组合实现:
- ト → U+FF8D(HALFWIDTH KATAKANA LETTER TO)
- ケ → U+FF7B(HALFWIDTH KATAKANA LETTER KE)
- ゙ → U+FF9E(HALFWIDTH KATAKANA VOICED SOUND MARK)
因此,字符串 'トゲ' 实际等价于 'ト' + 'ケ' + '゙',共 3 个 UTF-16 码元,而非 2 个“字符”。我们可通过以下代码验证:
const s = 'トゲ';
console.log(s.length); // 3
console.log([...s]); // ['ト', 'ケ', '゙'] —— 扩展运算符正确按 Unicode 码点分割
console.log(Array.from(s)); // ['ト', 'ケ', '゙']
console.log(s.charAt(0)); // 'ト'
console.log(s.charAt(1)); // 'ケ' ← 正确!索引 1 对应第二个码元
console.log(s.indexOf('ケ')); // 1 ← 因为 'ケ' 位于位置 1
console.log(s.indexOf('゙')); // 2
⚠️ 注意事项:
- String.prototype.charAt() 和 String.prototype.indexOf() 均以 UTF-16 码元索引 为准,不进行字形合成识别;
- 浏览器渲染时会将 KE + VOICED MARK(U+FF7B + U+FF9E)自动连字显示为视觉上的「ゲ」,但这属于渲染层行为,JavaScript 引擎层面仍将其视为两个独立字符;
- 若需按用户感知的“字符”(即 Unicode 标量值或字素簇)操作,应使用 Array.from(str)、[...str] 或 Intl.Segmenter(现代环境),避免依赖 charAt/substring 等传统方法;
- 全宽「ゲ」(U+30B2)是单个码点,'ゲ'.length === 1,行为符合直觉,但半宽形式无此对应。
✅ 总结:'トゲ'.indexOf('ケ') === 1 是完全符合规范的预期行为——它准确反映了 KE 在字符串 UTF-16 序列中的物理位置。开发中处理日文(尤其半宽假名、组合字符、emoji 等)时,务必区分「码元(code unit)」「码点(code point)」与「字素簇(grapheme cluster)」三层抽象,优先采用支持 Unicode-aware 的现代 API(如 for…of 循环、Array.from()、String.prototype.codePointAt())保障健壮性。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











