JavaScript 字符串为什么按码元计数?正则的 lastIndex 为什么会串结果?

先理解原理和示例,再练习完整口述与追问。

字符串按 UTF-16 码元存储,补充平面的字符(例如多数 emoji)占两个码元,length 和 charAt 数的是码元。码点要用 for...of、展开、codePointAt 和 Array.from。带 g 或 y 的正则会更新 lastIndex,同一个正则对象连续 test/exec 会从上次位置继续,用完要归零或每次新建。

Unicode 码点是字符编号,UTF-16 码元是 16 位存储单位。基本平面字符一个码元,emoji、部分生僻字需要代理对,两个码元组成一个码点。'😀'.length 是 2,[...'😀'].length 是 1。按“字符数”截断昵称、短信条数时,按 length 会把一个字符切成两半,界面出现乱码。比较用户输入的重音字符时,视觉相同的字符串可能是合成字符或预组合字符,比较前用 normalize('NFC')。

/a/g 这种全局正则是有状态的。exec 和 test 成功后把 lastIndex 设到匹配结束位置,下次从那里开始;匹配失败才把 lastIndex 设回 0。所以对同一字符串连续两次 test,可能一次 true、一次 false。match 在全局时会改 lastIndex,matchAll 要求全局或粘滞标志,并返回迭代器。u 标志让 . 和量词按码点工作,否则 . 可能只匹配代理对的一半。

js
const name = '😀Ada'
console.log(name.length, Array.from(name).length) // 5 4,一个 emoji 占两个码元

const re = /a/g
console.log(re.test('ab'), re.lastIndex, re.test('ab')) // true 1 false
re.lastIndex = 0

截断应该写成 Array.from(name).slice(0, 2).join(''),得到 '😀A',而不是 name.slice(0, 2) 把 emoji 切坏。

把正则写成模块级 /.../g 并在请求里反复 test,会出现“第一次能校验、第二次失败”。split 的捕获组会把分隔符也放进结果。不要用 length 判断是否只含一个用户能看见的字符。

JavaScript 字符串按 UTF-16 码元计数,emoji 和部分生僻字占两个码元,所以 length、charAt 和 slice 都可能把一个字符劈开。要按字符处理,用 for...of、展开或 Array.from,它们走码点;视觉上相同的重音字符比较前先 normalize。全局和粘滞正则会改 lastIndex,连续 test 或 exec 会接着上次的位置,所以要么每次新建正则,要么用完把 lastIndex 设回 0。