为什么带有全局标志的 RegExp 会给出错误的结果?

IT技术 javascript regex
2020-12-26 00:31:42

当我使用全局标志和不区分大小写标志时,这个正则表达式有什么问题?查询是用户生成的输入。结果应该是 [true, true]。

var query = 'Foo B';
var re = new RegExp(query, 'gi');
var result = [];
result.push(re.test('Foo Bar'));
result.push(re.test('Foo Bar'));
// result will be [true, false]

var reg = /^a$/g;
for(i = 0; i++ < 10;)
   console.log(reg.test("a"));

6个回答

RegExp带有g标志对象会跟踪lastIndex匹配发生的位置,因此在后续匹配中,它将从上次使用的索引开始,而不是从 0 开始。看看:

var query = 'Foo B';
var re = new RegExp(query, 'gi');
var result = [];
result.push(re.test('Foo Bar'));

alert(re.lastIndex);

result.push(re.test('Foo Bar'));

如果您不想lastIndex在每次测试后手动重置为 0,只需移除g标志即可。

这是规范规定的算法(第 15.10.6.2 节):

RegExp.prototype.exec(字符串)

对正则表达式执行字符串的正则表达式匹配,并返回包含匹配结果的 Array 对象,如果字符串不匹配,则返回 null 字符串 ToS​​tring(string) 搜索正则表达式模式的出现,如下所示:

  1. R成为这个 RexExp 对象。
  2. S为 ToString(string) 的值。
  3. 长度S的长度
  4. 让 lastIndex 是R上 lastIndex 属性的值
  5. 让 i 成为 ToInteger(lastIndex) 的值。
  6. 如果全局属性为假,则让 i = 0。
  7. 如果 i < 0 或 i > length,则将RlastIndex属性设置为 0 并返回 null。
  8. 调用 [[Match]],给它参数 S 和 i。如果[[Match]]返回失败,转步骤9;否则让 r 为其状态结果并转到步骤 10。
  9. 让 i = i+1。
  10. 转到步骤 7。
  11. 让 e 是 r 的 endIndex 值。
  12. 如果全局属性为真,则将RlastIndex属性设置为 e。
  13. 让 n 是 r 的捕获数组的长度。(这与 15.10.2.1 的 NCapturingParens 的值相同。)
  14. 返回一个具有以下属性的新数组:
  • index 属性设置为匹配子字符串在完整字符串 S 中的位置。
  • 输入属性设置为 S。
  • 长度属性设置为 n + 1。
  • 0 属性设置为匹配的子字符串(即 S 的偏移量 i inclusive 和 offset e excludes 之间的部分)。
  • 对于每个 i > 0 且 i ≤ n 的整数 i,将名为 ToString(i) 的属性设置为 r 的 captures 数组的第 i 个元素。
@IonuțG.Stan,对不起,如果我之前的评论看起来很冒犯,那不是我的意图。我现在无法编辑它,但我并不是要大喊大叫,只是为了引起人们对我评论的要点的注意。我的错!
2021-02-09 00:31:42
Firefox 的粘性标志根本不符合您的暗示。相反,它就像在正则表达式的开头有一个 ^ 一样,除了这个 ^ 匹配当前字符串位置 (lastIndex) 而不是字符串的开头。您正在有效地测试正则表达式是否匹配“就在此处”而不是“lastIndex 之后的任何地方”。查看您提供的链接!
2021-02-18 00:31:42
@Prestaul 是的,你说得对,它没有提到全局标志。由于问题的框架方式,这可能是(不记得我当时的想法)隐含的。随意编辑答案或将其删除并链接到您的答案。另外,让我向你保证,你比我好。享受!
2021-02-20 00:31:42
这个答案的开场白并不准确。您突出显示了规范的第 3 步,它什么也没说。的实际影响lastIndex在步骤 5、6 和 11 中。您的开场白仅在设置了全局标志的情况下才是正确的。
2021-03-01 00:31:42
这就像此处的 Galaxy API 设计漫游指南。“你陷入的陷阱已经在规范中完美记录了好几年,如果你只是费心去检查的话”
2021-03-08 00:31:42

您正在使用单个RegExp对象并多次执行它。在每次连续执行时,它会从最后一个匹配索引开始。

您需要在每次执行之前“重置”正则表达式以从头开始:

result.push(re.test('Foo Bar'));
re.lastIndex = 0;
result.push(re.test('Foo Bar'));
// result is now [true, true]

话虽如此,每次创建一个新的 RegExp 对象可能更具可读性(开销很小,因为无论如何都会缓存 RegExp):

result.push((/Foo B/gi).test(stringA));
result.push((/Foo B/gi).test(stringB));
或者干脆不使用该g标志。
2021-02-07 00:31:42

RegExp.prototype.test更新正则表达式的lastIndex属性,以便每个测试都从最后一个停止的地方开始。我建议使用,String.prototype.match因为它不会更新lastIndex属性:

!!'Foo Bar'.match(re); // -> true
!!'Foo Bar'.match(re); // -> true

注意:!!将其转换为布尔值,然后反转布尔值以反映结果。

或者,您可以重置该lastIndex属性:

result.push(re.test('Foo Bar'));
re.lastIndex = 0;
result.push(re.test('Foo Bar'));

删除全局g标志将解决您的问题。

var re = new RegExp(query, 'gi');

应该

var re = new RegExp(query, 'i');

使用 /g 标志告诉它在命中后继续搜索。

如果匹配成功,exec() 方法返回一个数组并更新正则表达式对象的属性。

在您第一次搜索之前:

myRegex.lastIndex
//is 0

第一次搜索后

myRegex.lastIndex
//is 8

删除 g 并在每次调用 exec() 后退出搜索。

OP 没有使用exec.
2021-02-28 00:31:42