JavaScript中Global-Flag RegExp匹配同样字符串结果不同问题
0x00 TL;DR
是因为对于有 g
flag 的正则表达式对象,RegExp#lastIndex
的值会随着匹配而改变。
0x01 问题详述
有个需求是把字符串中所有的非英文且非数字的字符转义。于是手撸了段 RegExp:
1 | const SHOULD_ESCAPE = /[\u0001-\u002f]|[\u003a-\u0040]|[\u005b-\u0060]|[\u007b-\uffff]/g; |
其实就是全部 UTF-8 字符刨去了 \u0030-\u0039
(0-9)、\u0041-\u005a
(A-Z)和 \u0061-\u007a
(a-z)三段。
但是在 node 里测试的时候发现了问题:
1 | SHOULD_ESCAPE.test("#"); // true |
测到这里我还以为是正则表达式写错了。但是接下来的事情就不太对劲了:
1 | SHOULD_ESCAPE.test("?"); // true |
我:???node 出 bug 了?
于是先考虑换个环境再测试一下:
1 | $ node -v |
node 版本 10.15.3(LTS)复现成功。
再换个环境:
1 | $ ssh [email protected] |
看来这是个 feature,不是个 bug。但是以本人多次学习 RegExp 的经验(四年前在 W3School,两年前在廖雪峰大大的博客,以及在 JS 实现了正则表达式反向引用特性的时候在 webreference 上又复习了一遍),似乎没有任何信息提到了这个莫名其妙的 feature。
0x02 解释
【此处省略面向搜索引擎编程 5 分钟】
其实这是因为对于有着 Global-Flag(也就是 /abc/g
这样的正则表达式),JavaScript 会在这个正则表达式对象上创建一个 lastIndex
属性。这个属性记录了上次匹配到的下标,下次匹配时会从这个下标继续开始匹配,失败自动置零。
所以真正的匹配过程是这样的:
1 | SHOULD_ESCAPE.lastIndex; // 0 |
那么了解了这个知识点之后,最后形成的匹配 + 替换代码如下:
1 | const SHOULD_ESCAPE = /[\u0001-\u002f]|[\u003a-\u0040]|[\u005b-\u0060]|[\u007b-\uffff]/g; |
(我知道在 replacer
函数里修改外层 RegExp 对象可能会破坏优化……但是没办法,需求所迫啊(逃