charAt()索引越界返回空字符串''而非undefined或报错,易致脱敏逻辑静默错误;需手动校验索引有效性,如maskPhone中须确保phone.length≥8才安全访问charAt(3)~charAt(6)。

charAt() 提取单字符的边界行为必须清楚
charAt() 返回指定索引处的单个字符(string 类型),索引超出范围时返回空字符串 '',不是 undefined 或报错。这点在脱敏逻辑中极易引发静默错误——比如想隐藏手机号第4~7位,却因索引算错导致拼接出带空字符串的异常结果。
常见误用:phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') 更稳妥,但若坚持用 charAt() 拼接,必须手动校验索引有效性:
function maskPhone(phone) {
if (phone.length < 11) return phone;
return (
phone.charAt(0) +
phone.charAt(1) +
phone.charAt(2) +
'****' +
phone.charAt(7) +
phone.charAt(8) +
phone.charAt(9) +
phone.charAt(10)
);
}
- 索引从
0开始,charAt(11)在 11 位手机号上会返回'' - 不要依赖
charAt(-1)取末位——它等价于charAt(0),负索引被忽略 - 中文、emoji 等 Unicode 字符可能占多个 UTF-16 码元,
charAt()按码元操作,对 ? 这类字符可能只取到前半部分,显示为
用 charAt() 做简单脱敏比 substring() 更轻量但更易出错
如果只是「保留前3位 + 后4位,中间打星」,charAt() 看似直接,实则要写 7 次调用;而 substring(0, 3) + '****' + substring(7) 更简洁且语义清晰。但某些嵌入式环境或严格性能场景(如高频日志脱敏)中,避免创建子字符串对象确实有收益。
关键权衡点:
-
charAt(i)返回string,但不创建新字符串对象(V8 中通常复用原字符串内部表示) -
substring()必然生成新字符串,内存开销略高 - 但手写一长串
charAt()拼接,可读性差、维护成本高,容易漏掉某个索引
处理非 ASCII 字符时 charAt() 的陷阱
遇到用户昵称含中文、日文或 emoji 时,charAt() 的“位置”是 UTF-16 码元位置,不是字符位置。例如 '??'.length === 4,但实际是 1 个字符(由多个码元组成)。此时 charAt(0) 返回 ''(代理对高位),charAt(1) 是低位,单独输出即乱码。
若需按真实字符数脱敏(如「昵称显示前2字+***」),必须用支持 Unicode 的方法:
- 用
Array.from(nick).slice(0, 2).join('')替代nick.charAt(0) + nick.charAt(1) - 或正则
nick.match(/^[\s\S]{0,2}/)[0](兼容性更好) - 直接放弃
charAt()—— 它在多语言场景下本质不可靠
服务端 Node.js 中慎用 charAt() 做敏感字段清洗
Node.js 的 String.prototype.charAt() 行为与浏览器一致,但服务端常需处理数据库原始字段(如身份证号、银行卡号),这些字段可能含不可见字符(BOM、零宽空格等)。charAt() 会把它们当普通字符提取,导致脱敏后仍泄露结构信息。
更安全的做法:
- 清洗输入:用
input.replace(/[\u200B-\u200F\u202A-\u202E]/g, '')先剔除零宽字符 - 再统一用
replace()正则做模式化脱敏,而非逐字符拼接 - 对银行卡号等固定格式,优先用
input.replace(/^(.{4})\d{8}(.{4})$/, '$1********$2')
字符级操作在敏感信息处理中属于“能不用就不用”的层级——它太底层,容错率低,一旦索引偏移或编码混杂,脱敏就形同虚设。

















