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
2
SHOULD_ESCAPE.test("#");  // true
SHOULD_ESCAPE.test("?"); // false

测到这里我还以为是正则表达式写错了。但是接下来的事情就不太对劲了:

1
2
3
4
SHOULD_ESCAPE.test("?");  // true
SHOULD_ESCAPE.test("?"); // false
SHOULD_ESCAPE.test("?"); // true
SHOULD_ESCAPE.test("?"); // false

我:???node 出 bug 了?

于是先考虑换个环境再测试一下:

1
2
3
4
5
6
7
8
9
10
11
$ node -v
v10.12.0
$ nvm install 10.15.3
$ node -v
v10.15.3
$ node
> const SHOULD_ESCAPE = /[\u0001-\u002f]|[\u003a-\u0040]|[\u005b-\u0060]|[\u007b-\uffff]/g;
> SHOULD_ESCAPE.test("?");
true
> SHOULD_ESCAPE.test("?");
false

node 版本 10.15.3(LTS)复现成功。

再换个环境:

1
2
3
4
5
6
7
8
9
$ ssh [email protected]
[email protected] $ node -v
v12.0.0
[email protected] $ node
> const SHOULD_ESCAPE = /[\u0001-\u002f]|[\u003a-\u0040]|[\u005b-\u0060]|[\u007b-\uffff]/g;
> SHOULD_ESCAPE.test("?");
true
> SHOULD_ESCAPE.test("?");
false

看来这是个 feature,不是个 bug。但是以本人多次学习 RegExp 的经验(四年前在 W3School,两年前在廖雪峰大大的博客,以及在 JS 实现了正则表达式反向引用特性的时候在 webreference 上又复习了一遍),似乎没有任何信息提到了这个莫名其妙的 feature。

0x02 解释

【此处省略面向搜索引擎编程 5 分钟】

其实这是因为对于有着 Global-Flag(也就是 /abc/g 这样的正则表达式),JavaScript 会在这个正则表达式对象上创建一个 lastIndex 属性。这个属性记录了上次匹配到的下标,下次匹配时会从这个下标继续开始匹配,失败自动置零。

所以真正的匹配过程是这样的:

1
2
3
4
5
6
SHOULD_ESCAPE.lastIndex;  // 0
SHOULD_ESCAPE.test("?"); // 从头开始匹配,匹配成功,返回 true
SHOULD_ESCAPE.lastIndex; // 1
SHOULD_ESCAPE.test("?"); // "?" 没有下标为 1 的字符,所以匹配失败,返回 false
SHOULD_ESCAPE.lastIndex; // 0
SHOULD_ESCAPE.test("?"); // 从头开始匹配,匹配成功,返回 true

那么了解了这个知识点之后,最后形成的匹配 + 替换代码如下:

1
2
3
4
5
6
7
8
const SHOULD_ESCAPE = /[\u0001-\u002f]|[\u003a-\u0040]|[\u005b-\u0060]|[\u007b-\uffff]/g;
function replaceAll(dir) {
return dir.replace(SHOULD_ESCAPE, function (str) {
// see https://stackoverflow.com/a/13587801/10403554
SHOULD_ESCAPE.lastIndex = 0;
return "$" + str.charCodeAt(0).toString(16);
});
}

(我知道在 replacer 函数里修改外层 RegExp 对象可能会破坏优化……但是没办法,需求所迫啊(逃

来源:https://blog.jiejiss.com/