
本文解释为何 'トゲ'.indexOf('ケ') 返回 1 且 'トゲ'.charAt(1) 返回 'ケ':字符串 'トゲ' 实际由三个 Unicode 字符组成(半宽ト、半宽ケ、半宽 voiced sound mark ゚),而非视觉上看似两个字符的合成字,因此索引操作严格按码点单位计算。
本文解释为何 `'トゲ'.indexof('ケ')` 返回 1 且 `'トゲ'.charat(1)` 返回 `'ケ'`:字符串 `'トゲ'` 实际由三个 unicode 字符组成(半宽ト、半宽ケ、半宽 voiced sound mark `゚`),而非视觉上看似两个字符的合成字,因此索引操作严格按码点单位计算。
在 JavaScript 中,String.prototype.indexOf() 和 String.prototype.charAt() 均基于 UTF-16 编码单元(code units) 进行操作,而非用户感知的“视觉字符”或 Unicode 抽象字符(grapheme clusters)。这一点对处理日文半宽片假名(Half-width Katakana)尤为关键。
以字符串 'トゲ' 为例,它并非由两个字符 ト 和 ゲ 构成,而是由以下 三个独立的 Unicode 字符 组成:
- ト — U+FF86(HALFWIDTH KATAKANA LETTER TO)
- ケ — U+FF7B(HALFWIDTH KATAKANA LETTER KE)
- ゙ — U+FF9E(HALFWIDTH KATAKANA VOICED SOUND MARK,即半宽浊音符)
⚠️ 注意:Unicode 中不存在独立的“半宽ゲ”字符(U+FF7B + U+FF9E 的组合是标准表示法),而全宽版 ゲ 则有专用码点 U+30B2。浏览器/渲染引擎会将 ケ + ゙ 连续显示为视觉上连体的 ゲ,但这只是渲染层的组合效果,底层仍是两个可单独寻址的字符。
因此:
立即学习“Java免费学习笔记(深入)”;
'トゲ'.length // → 3(三个 UTF-16 code units)
'トゲ'.charAt(0) // → 'ト'
'トゲ'.charAt(1) // → 'ケ' ← 正确返回第二个字符
'トゲ'.charAt(2) // → '゙'
'トゲ'.indexOf('ケ') // → 1 ← 因为 'ケ' 确实位于索引 1 处
'トゲ'.indexOf('゙') // → 2
'トゲ'.indexOf('ゲ') // → -1 ← 'ゲ' 是两个字符,不是单个子串(除非写成 'ゲ' 字符串,但此处源串中并无该预组合字符)✅ 正确判断“是否包含半宽ゲ”的方式应基于字符序列匹配,而非单字符查找:
const str = 'トゲ';
// 检查是否包含「ケ+浊音符」这一组合
const hasKeGe = /ゲ/.test(str.replace(/[\uFF9E\uFF9F]/g, '゙')); // 不推荐:依赖替换
// 更健壮的做法:显式检查相邻字符
function containsHalfwidthKeGe(s) {
for (let i = 0; i < s.length - 1; i++) {
if (s.charCodeAt(i) === 0xFF7B && s.charCodeAt(i + 1) === 0xFF9E) {
return true;
}
}
return false;
}
console.log(containsHalfwidthKeGe('トゲ')); // true? 总结:
- JavaScript 字符串操作始终面向 编码单元(code units),不是视觉字形;
- 半宽片假名的浊音/半浊音通过基础字符 + 附加符号(U+FF9E/U+FF9F) 实现,属 Unicode 组合机制;
- 如需按“用户看到的一个字”进行操作(如光标移动、删除、高亮),应使用 Intl.Segmenter(现代环境)或成熟的 grapheme-splitting 库(如 grapheme-splitter);
- 避免用 charAt() / indexOf() 直接处理东亚文字中的组合字符场景——它们不理解字素簇(grapheme clusters)。


















