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


277

当我使用全局标志和不区分大小写的标志时,此正则表达式有什么问题?查询是用户生成的输入。结果应该是[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"));


54
欢迎使用JavaScript中的RegExp众多陷阱之一。它具有我遇到过的最糟糕的正则表达式处理接口之一,充满了怪异的副作用和难以理解的警告。您通常要用正则表达式执行的大多数常见任务很难正确拼写。
bobince

XRegExp看起来是一个不错的选择。xregexp.com
大约


一种解决方案(如果可以解决的话)是直接使用regex文字,而不是将其保存到re
thdoan '18 -10-10

Answers:


350

RegExp对象跟踪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。在字符串ToString(string)中搜索正则表达式模式的出现,如下所示:

  1. 令S为ToString(string)的值。
  2. 令length为S的长度。
  3. 令lastIndex为lastIndex属性的值。
  4. 令我为ToInteger(lastIndex)的值。
  5. 如果全局属性为false,则让i = 0。
  6. 如果I <0或I> length,则将lastIndex设置为0并返回null。
  7. 调用[[Match]],为其指定参数S和i。如果[[Match]]返回失败,请转到步骤8;否则,请转到步骤8。否则,使r为其State结果,然后转到步骤10。
  8. 令i = i + 1。
  9. 转到步骤6。
  10. 令e为r的endIndex值。
  11. 如果global属性为true,则将lastIndex设置为e。
  12. 令n为r的捕获数组的长度。(此值与15.10.2.1的NCapturingParens相同。)
  13. 返回具有以下属性的新数组:
    • index属性设置为匹配的子字符串在完整字符串S中的位置。
    • 输入属性设置为S。
    • length属性设置为n + 1。
    • 将0属性设置为匹配的子字符串(即,偏移量i包含在内和偏移量e排除之间的S部分)。
    • 对于使i> 0并且I≤n的每个整数i,将名为ToString(i)的属性设置为r的捕获数组的第i个元素。

83
这就像这里的Hitchhiker Galaxy API设计指南。“如果您只想检查一下,那么您陷入的那个陷阱已经在规范中完美记录了好几年了”
Retsam 2013年

5
Firefox的粘滞标记完全没有您所暗示的。相反,它的作用就像在正则表达式的开头有一个^,除了此^匹配当前字符串位置(lastIndex)而不是字符串的开头。您正在有效地测试正则表达式是否匹配“就在这里”而不是“ lastIndex之后的任何地方”。查看您提供的链接!
Doin 2014年

1
这个答案的开篇陈述并不准确。您突出显示了规范的第3步,什么也没说。的实际影响lastIndex是在步骤5、6和11中。仅在设置了全局标志的情况下,您的开幕词才是正确的。
2014年

@Prestaul是的,您是对的,它没有提到全局标志。由于问题的框架方式,它可能(不记得我当时的想法)是隐式的。随意编辑答案或删除答案并链接到您的答案。另外,让我向您保证,您比我更好。请享用!
Ionuț G. Stan 2014年

@ IonuțG.Stan,很抱歉,如果我之前的评论似乎有攻击性,那不是我的意图。目前,我无法对其进行编辑,但我并未试图大喊大叫,只是为了吸引人们注意我的评论的重点。我的错!
Prestaul 2014年

72

您正在使用单个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));

1
或者根本不使用g标志。
melpomene

36

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'));

11

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

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

应该

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

0

您需要设置re.lastIndex = 0,因为使用g标志正则表达式跟踪发生的最后一次匹配,因此测试不会去测试相同的字符串,因此您需要这样做re.lastIndex = 0

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

console.log(result)



-1

我有功能:

function parseDevName(name) {
  var re = /^([^-]+)-([^-]+)-([^-]+)$/g;
  var match = re.exec(name);
  return match.slice(1,4);
}

var rv = parseDevName("BR-H-01");
rv = parseDevName("BR-H-01");

第一次通话有效。第二个电话没有。该slice操作抱怨为空值。我认为这是因为re.lastIndex。这很奇怪,因为我希望RegExp每次调用该函数时都会分配一个新的函数,并且不会在函数的多次调用之间共享。

当我将其更改为:

var re = new RegExp('^([^-]+)-([^-]+)-([^-]+)$', 'g');

然后我没有lastIndex保持效果。它按我期望的那样工作。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.