Warum liefert ein RegExp mit globalem Flag falsche Ergebnisse?

277

Was ist das Problem mit diesem regulären Ausdruck, wenn ich das globale Flag und das Flag ohne Berücksichtigung der Groß- und Kleinschreibung verwende? Die Abfrage ist eine vom Benutzer generierte Eingabe. Das Ergebnis sollte [wahr, wahr] sein.

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

Über
quelle
54
Willkommen zu einer der vielen RegExp-Fallen in JavaScript. Es hat eine der schlechtesten Schnittstellen zur Regex-Verarbeitung, die ich je getroffen habe, voller seltsamer Nebenwirkungen und obskurer Vorbehalte. Die meisten der allgemeinen Aufgaben, die Sie normalerweise mit Regex ausführen möchten, sind schwer richtig zu buchstabieren.
Bobince
XRegExp scheint eine gute Alternative zu sein. xregexp.com
ungefähr
Siehe Antwort auch hier: stackoverflow.com/questions/604860/…
Prestaul
Eine Lösung, wenn Sie damit durchkommen können, besteht darin, das Regex-Literal direkt zu verwenden, anstatt es zu speichern re.
10.

Antworten:

350

Das RegExpObjekt verfolgt, lastIndexwo eine Übereinstimmung stattgefunden hat. Bei nachfolgenden Übereinstimmungen beginnt es also mit dem zuletzt verwendeten Index anstelle von 0. Sehen Sie sich Folgendes an:

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

Wenn Sie nicht lastIndexnach jedem Test manuell auf 0 zurücksetzen möchten , entfernen Sie einfach das gFlag.

Hier ist der Algorithmus, den die Spezifikationen vorschreiben (Abschnitt 15.10.6.2):

RegExp.prototype.exec (Zeichenfolge)

Führt eine Übereinstimmung der Zeichenfolge mit regulären Ausdrücken mit dem regulären Ausdruck durch und gibt ein Array-Objekt zurück, das die Ergebnisse der Übereinstimmung enthält, oder null, wenn die Zeichenfolge nicht übereinstimmt. Die Zeichenfolge ToString (Zeichenfolge) wird wie folgt nach einem Vorkommen des Musters für reguläre Ausdrücke durchsucht:

  1. Sei S der Wert von ToString (String).
  2. Die Länge sei die Länge von S.
  3. Sei lastIndex der Wert der lastIndex-Eigenschaft.
  4. Sei i der Wert von ToInteger (lastIndex).
  5. Wenn die globale Eigenschaft false ist, sei i = 0.
  6. Wenn I <0 oder I> lang ist, setzen Sie lastIndex auf 0 und geben null zurück.
  7. Rufen Sie [[Match]] auf und geben Sie ihm die Argumente S und i. Wenn [[Match]] einen Fehler zurückgegeben hat, fahren Sie mit Schritt 8 fort. Andernfalls sei r das Statusergebnis und gehe zu Schritt 10.
  8. Sei i = i + 1.
  9. Fahren Sie mit Schritt 6 fort.
  10. Sei e der endIndex-Wert von r.
  11. Wenn die globale Eigenschaft true ist, setzen Sie lastIndex auf e.
  12. Sei n die Länge des Captures-Arrays von r. (Dies ist der gleiche Wert wie bei NCapturingParens vom 15.10.2.1.)
  13. Geben Sie ein neues Array mit den folgenden Eigenschaften zurück:
    • Die index-Eigenschaft wird auf die Position des übereinstimmenden Teilstrings innerhalb des vollständigen Strings S gesetzt.
    • Die Eingabeeigenschaft wird auf S gesetzt.
    • Die Länge-Eigenschaft wird auf n + 1 gesetzt.
    • Die 0-Eigenschaft wird auf den übereinstimmenden Teilstring gesetzt (dh den Teil von S zwischen Offset i einschließlich und Offset e exklusiv).
    • Setzen Sie für jede ganze Zahl i, so dass I> 0 und I ≤ n ist, die Eigenschaft ToString (i) auf das i-te Element des Captures-Arrays von r.
Ionuț G. Stan
quelle
83
Dies ist wie der Per Anhalter durch das Galaxy API-Design hier. "Diese Falle, in die Sie geraten sind, ist seit mehreren Jahren in der Spezifikation perfekt dokumentiert, wenn Sie sich nur die Mühe gemacht hätten, dies zu überprüfen"
Retsam,
5
Die klebrige Flagge von Firefox macht überhaupt nicht das, was Sie implizieren. Es verhält sich eher so, als ob am Anfang des regulären Ausdrucks ein ^ steht, AUSSER, dass dieses ^ eher mit der aktuellen Zeichenfolgenposition (lastIndex) als mit dem Anfang der Zeichenfolge übereinstimmt. Sie testen effektiv, ob der reguläre Ausdruck "genau hier" statt "irgendwo nach lastIndex" übereinstimmt. Siehe den Link, den Sie bereitgestellt haben!
Doin
1
Die einleitende Aussage dieser Antwort ist einfach nicht korrekt. Sie haben Schritt 3 der Spezifikation hervorgehoben, der nichts sagt. Der tatsächliche Einfluss von lastIndexist in den Schritten 5, 6 und 11. Ihre Eröffnungserklärung ist nur wahr, wenn die globale Flagge gesetzt ist.
Prestaul
@Prestaul ja, du hast recht, dass die globale Flagge nicht erwähnt wird. Es war wahrscheinlich (kann mich nicht erinnern, was ich damals gedacht habe) implizit aufgrund der Art und Weise, wie die Frage umrahmt ist. Sie können die Antwort jederzeit bearbeiten oder löschen und mit Ihrer Antwort verknüpfen. Lassen Sie mich Ihnen auch versichern, dass Sie besser sind als ich. Genießen!
Ionuț G. Stan
@ IonuțG.Stan, sorry, wenn mein vorheriger Kommentar angreifend schien, war das nicht meine Absicht. Ich kann es an dieser Stelle nicht bearbeiten, aber ich habe nicht versucht zu schreien, nur um die Aufmerksamkeit auf den wesentlichen Punkt meines Kommentars zu lenken. Mein Fehler!
Prestaul
72

Sie verwenden ein einzelnes RegExpObjekt und führen es mehrmals aus. Bei jeder aufeinanderfolgenden Ausführung wird ab dem letzten Übereinstimmungsindex fortgesetzt.

Sie müssen den regulären Ausdruck "zurücksetzen", um vor jeder Ausführung von vorne zu beginnen:

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

Allerdings ist es möglicherweise besser lesbar, jedes Mal ein neues RegExp-Objekt zu erstellen (der Overhead ist minimal, da RegExp ohnehin zwischengespeichert wird):

result.push((/Foo B/gi).test(stringA));
result.push((/Foo B/gi).test(stringB));
Roatin Marth
quelle
1
Oder benutze einfach nicht die gFlagge.
Melpomene
36

RegExp.prototype.testAktualisiert die lastIndexEigenschaft der regulären Ausdrücke, sodass jeder Test dort beginnt, wo der letzte gestoppt wurde. Ich würde vorschlagen, zu verwenden, String.prototype.matchda es die lastIndexEigenschaft nicht aktualisiert :

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

Hinweis: !!Konvertiert es in einen Booleschen Wert und invertiert dann den Booleschen Wert, sodass er das Ergebnis widerspiegelt.

Alternativ können Sie die lastIndexEigenschaft auch einfach zurücksetzen :

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

Durch Entfernen des globalen gFlags wird Ihr Problem behoben.

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

Sollte sein

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

Sie müssen re.lastIndex = 0 setzen, da mit g flag regex die letzte aufgetretene Übereinstimmung verfolgt wird, sodass beim Test nicht dieselbe Zeichenfolge getestet wird. Dazu müssen Sie re.lastIndex = 0 ausführen

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)

Ashish
quelle
-1

Wenn Sie das Flag / g verwenden, wird die Suche nach einem Treffer fortgesetzt.

Wenn die Übereinstimmung erfolgreich ist, gibt die exec () -Methode ein Array zurück und aktualisiert die Eigenschaften des Objekts mit regulären Ausdrücken.

Vor Ihrer ersten Suche:

myRegex.lastIndex
//is 0

Nach der ersten Suche

myRegex.lastIndex
//is 8

Entfernen Sie das g und es beendet die Suche nach jedem Aufruf von exec ().

Scott Schlechtleitner
quelle
OP verwendet nicht exec.
Melpomene
-1

Ich hatte die Funktion:

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

Der erste Anruf funktioniert. Der zweite Anruf nicht. Die sliceOperation beschwert sich über einen Nullwert. Ich nehme an, das liegt an der re.lastIndex. Dies ist seltsam, da ich erwarten würde, dass RegExpbei jedem Aufruf der Funktion eine neue zugewiesen wird und nicht für mehrere Aufrufe meiner Funktion freigegeben wird.

Als ich es geändert habe zu:

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

Dann bekomme ich den lastIndexHoldover-Effekt nicht. Es funktioniert so, wie ich es erwarten würde.

Chelmite
quelle