先记住这个答案
g 和 y 都让正则对象带上状态,exec 和 test 都从 lastIndex 开始匹配,成功后把 lastIndex 挪到匹配末尾,失败后归零。区别只有一个:g 在当前位置匹配不上时会继续往右找,y 不会,它要求匹配必须正好从 lastIndex 开始。y 是 sticky 的缩写,意思是「粘在这个位置上」。它最典型的用途是写词法分析器,一段一段连续地读取输入,遇到认不出的字符能立刻知道出错位置,而不是跳过去。
g从lastIndex往后搜,y只在lastIndex处试- 两者成功都挪
lastIndex,失败都归零 y的典型用途是逐个读取 token 的词法分析^只认整串开头,和y不是一回事
同一个起点,g 和 y 的不同反应
先把两个正则的 lastIndex 都设成 1,而目标字符在下标 2,看看它们怎么处理。
aJavaScriptconst text = 'x a'
const global = /a/g
const sticky = /a/y
global.lastIndex = 1
sticky.lastIndex = 1
console.log(global.exec(text)?.index, global.lastIndex) // 往后找到下标 2
console.log(sticky.exec(text), sticky.lastIndex) // 下标 1 是空格,直接失败
sticky.lastIndex = 2
console.log(sticky.exec(text)?.index, sticky.lastIndex) // 起点对准了才成功查看输出与解释
2 3
null 0
2 3g 从下标 1 开始,发现是空格,于是往右移一位,在下标 2 找到了 a。y 在下标 1 就要求必须是 a,是空格就失败,lastIndex 归零。把起点改到 2 之后 y 才成功。
可以这样类比:g 像在一本书里从书签处往后翻着找某个词,y 像逐字朗读,读到的这个字必须是预期的那个,不是就停下报错。
y 的真正用途:写词法分析器
词法分析就是把一串字符切成一个个有意义的单元,比如把 12 + 3*4 切成 12、+、3、*、4。它最重要的要求是不能漏:每个字符都必须被某条规则认领,认不出来就要报错。用 g 做不到这一点,因为它会悄悄跳过认不出的字符。
const token = /\s*(\d+|[+\-*\/])/y // 先吃掉空白,再读一个数字或运算符
function tokenize(src) {
const out = []
token.lastIndex = 0 // 每次新输入先归零
while (token.lastIndex < src.length) {
const start = token.lastIndex
const m = token.exec(src)
if (!m) throw new SyntaxError(`第 ${start} 位无法识别`)
out.push(m[1])
}
return out
}
console.log(tokenize('12 + 3*4').join(' '))
try { tokenize('1 + x') } catch (e) { console.log(e.message) }查看输出与解释
12 + 3 * 4
第 3 位无法识别每次 exec 成功,lastIndex 正好停在这个 token 的末尾,下一轮就从这里接着读,不需要手动截字符串。读到 1 + x 的下标 3 时,\s* 吃掉空格后遇到 x,数字和运算符都对不上,exec 返回 null,我们立刻知道出错位置在哪。
如果把这里的 y 换成 g,输入 1 + x 2 就不会报错了:exec 遇到 x 对不上,会继续往右找,直接读到后面的 2,最后得到 1 + 2。非法字符被悄悄吞掉,这正是词法分析最不能接受的。
以前没有 y 时,常见做法是每读一个 token 就 src.slice(pos) 截出剩余部分,再用 ^ 开头的正则去匹配。这样还得自己维护 pos,每轮都多一次截取。y 直接在原字符串上用 lastIndex 定位,读取位置由正则自己推进,代码更短也更不容易算错偏移。
容易答错的地方
- 以为
y和^是一回事 ^永远只匹配整个字符串的开头(开了m标志时是每行开头),不管lastIndex是多少。/^a/y把lastIndex设成 2 去测'baa',结果是false,因为下标 2 不是字符串开头。y的锚点是可以移动的lastIndex,所以才能一段段往后读。- 以为
y会自动跳过空白 y只在lastIndex处原地尝试。上面的 tokenizer 里规则开头写了\s*,就是为了把空白也吃掉。如果去掉它,12 + 3读完12之后下标 2 是空格,直接失败。另外要当心能匹配空字符串的规则,它成功了却不推进lastIndex,循环会原地打转。
面试官还会怎么问?
g 和 y 能一起用吗?
可以,而且在 match、replace 里有明确的效果:只拿从头开始连续挨着的那些匹配,中间一断就停。'aaba'.match(/a/g) 找到 3 个 a,'aaba'.match(/a/gy) 只找到前 2 个,因为下标 2 的 b 把连续性打断了。'aaba'.replace(/a/gy, '-') 的结果是 --ba。
怎么判断一个正则有没有 y 标志?
读 re.sticky 属性,返回布尔值。也可以看 re.flags,它会按固定顺序列出所有标志,比如 /a/yg 的 flags 是 gy。
为什么 matchAll 不太适合做词法分析?
matchAll 必须带 g,即使同时带 y,它也只会在匹配断开时静默结束,你拿不到「在第几位断开」这个信息,也没法在中途根据上一个 token 切换规则。词法分析需要在每一步检查失败位置、有时还要换一条规则去试,用 exec 加 lastIndex 手动控制更合适。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。