at()按逻辑字符索引并安全提取,codePointAt()按UTF-16码元位置返回码点;二者协同可遍历字符串,通过判断码点是否≥0x10000确定每个逻辑字符的起始位置和长度(1或2个码元),从而还原其物理边界。

String.prototype.at() 和 codePointAt 解决的是不同层级的问题:前者按“逻辑字符”(即 Unicode 码点序列)索引,后者按“字节偏移”返回码点值。它们不能直接配合使用来“判定物理边界”,但可以协同辅助识别字符串中每个逻辑字符的起始位置和长度(以 UTF-16 编码单位计),从而间接还原其在内存中的物理布局。
理解“物理边界”的真实含义
JavaScript 字符串内部以 UTF-16 编码存储,一个 Unicode 码点若大于 0xFFFF(即需要 4 字节 UTF-8 或两个 UTF-16 代码单元),就会被拆成一对代理对(surrogate pair)。所谓“物理边界”,指的是该字符在字符串中占据的 UTF-16 代码单元(code unit)起始索引和跨度(1 或 2)。
- ASCII 字符(如 a)→ 占 1 个 code unit,
"a".codePointAt(0)返回 97,"a".length === 1 - emoji 如 ?(U+1F44D)→ 占 2 个 code unit(0xD83D 0xDC4D),
"?".codePointAt(0)返回 128077,"?".length === 2 -
at()接收逻辑索引(从 0 开始数第几个字符),自动跳过代理对的高位单元;而codePointAt()接收物理索引(UTF-16 code unit 位置),需手动避开低位代理单元才能得到正确码点
用 codePointAt 遍历并标记每个逻辑字符的物理起始与长度
核心思路:从索引 i = 0 开始循环,每次调用 codePointAt(i) 得到当前逻辑字符的码点,再根据该码点是否 ≥ 0x10000 判断它占几个 code unit,从而确定下一个字符的起始位置。
- 若
cp < 0x10000→ 当前字符占 1 个 code unit,下个字符从i + 1开始 - 若
cp >= 0x10000→ 是辅助平面字符,占 2 个 code unit,下个字符从i + 2开始 - 这样就能生成每个逻辑字符的
{ start: i, length: 1 or 2, codePoint: cp }元组,即完整物理边界信息
at() 的作用是安全读取逻辑字符,而非定位
str.at(n) 等价于 Array.from(str)[n],它内部已处理代理对,确保返回完整的逻辑字符(如单个 emoji)。但它不暴露索引偏移,也不能告诉你这个字符在原始字符串里占几位。所以:
- ✅ 用
at()安全提取第 n 个用户感知的字符(避免切开代理对) - ❌ 不能用
at()反推该字符的起始 code unit 位置 - ✅ 若需物理边界,必须用
codePointAt()正向扫描,配合手动步进
实用示例:获取字符串中每个字符的物理范围
以下函数返回形如 [{char: "?", start: 0, length: 2}, {char: "a", start: 2, length: 1}] 的数组:
function getCharBoundaries(str) {
const result = [];
let i = 0;
while (i < str.length) {
const cp = str.codePointAt(i);
const char = String.fromCodePoint(cp);
const length = cp > 0xFFFF ? 2 : 1;
result.push({ char, start: i, length, codePoint: cp });
i += length;
}
return result;
}
这个结果就是你所需的“原始字符物理边界”——每个逻辑字符对应的 UTF-16 起始位置与宽度,可用于安全截断、光标定位、文本渲染分块等底层操作。

















