Dlaczego RegExp z flagą globalną daje nieprawidłowe wyniki?

277

Na czym polega problem z tym wyrażeniem regularnym, gdy używam flagi globalnej i flagi bez rozróżniania wielkości liter? Zapytanie to dane wejściowe generowane przez użytkownika. Wynik powinien być [prawda, prawda].

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

o
źródło
54
Witamy w jednej z wielu pułapek RegExp w JavaScript. Ma jeden z najgorszych interfejsów do przetwarzania wyrażeń regularnych, jakie kiedykolwiek spotkałem, pełen dziwnych efektów ubocznych i niejasnych zastrzeżeń. Większość typowych zadań, które zwykle chcesz wykonywać za pomocą wyrażenia regularnego, trudno jest poprawnie przeliterować.
bobince
XRegExp wygląda na dobrą alternatywę. xregexp.com
około
Zobacz także odpowiedź tutaj: stackoverflow.com/questions/604860/…
Prestaul
Jednym z rozwiązań, jeśli można sobie z tym poradzić, jest bezpośrednie użycie literału regularnego zamiast zapisywania go re.
thdoan

Odpowiedzi:

350

RegExpObiekt śledzi lastIndexgdzie wystąpił mecz, więc w kolejnych meczach będzie zacząć od ostatniego indeksu używane zamiast 0. Spójrz:

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

Jeśli nie chcesz ręcznie resetować lastIndexdo 0 po każdym teście, po prostu usuń gflagę.

Oto algorytm, który dyktują specyfikacje (sekcja 15.10.6.2):

RegExp.prototype.exec (ciąg)

Wykonuje dopasowanie wyrażenia regularnego w stosunku do wyrażenia regularnego i zwraca obiekt Array zawierający wyniki dopasowania lub wartość NULL, jeśli ciąg nie jest zgodny. Ciąg ToString (ciąg) jest szukany pod kątem wystąpienia wzorca wyrażenia regularnego w następujący sposób:

  1. Niech S będzie wartością ToString (string).
  2. Niech długość będzie długością S.
  3. Niech lastIndex będzie wartością właściwości lastIndex.
  4. Niech będę wartością ToInteger (lastIndex).
  5. Jeśli właściwość globalna ma wartość false, niech i = 0.
  6. Jeśli I <0 lub I> długość, ustaw lastIndex na 0 i zwróć null.
  7. Wywołaj [[Dopasuj]], podając argumenty S i i. Jeśli [[Dopasuj]] zwrócił błąd, przejdź do kroku 8; w przeciwnym razie niech będzie wynikiem stanu i przejdź do kroku 10.
  8. Niech i = i + 1.
  9. Przejdź do kroku 6.
  10. Niech będzie końcową wartością indeksu.
  11. Jeśli właściwość globalna ma wartość true, ustaw lastIndex na e.
  12. Niech n będzie długością tablicy przechwytywania r. (Jest to ta sama wartość, co NCapturingParens z 15.10.2.1.)
  13. Zwróć nową tablicę o następujących właściwościach:
    • Właściwość index jest ustawiona na pozycję dopasowanego podłańcucha w całym ciągu S.
    • Właściwość wejściowa jest ustawiona na S.
    • Właściwość length jest ustawiona na n + 1.
    • Właściwość 0 jest ustawiona na dopasowany podłańcuch (tj. Część S między przesunięciem i włącznie i przesunięciem e).
    • Dla każdej liczby całkowitej i takiej, że I> 0 i I ≤ n, ustaw właściwość ToString (i) na i-ty element tablicy r przechwytywania.
Ionuț G. Stan
źródło
83
To jest jak przewodnik Autostopowicza po projekcie Galaxy API tutaj. „Ta pułapka, w którą wpadłeś, została doskonale udokumentowana w specyfikacji od kilku lat, jeśli tylko próbowałeś to sprawdzić”
Retsam
5
Lepka flaga Firefoksa wcale nie robi tego, co sugerujesz. Działa raczej tak, jakby na początku wyrażenia regularnego znajdowało się ^ Z WYJĄTKIEM, że to ^ pasuje do bieżącej pozycji łańcucha (lastIndex), a nie do początku łańcucha. Skutecznie testujesz, czy wyrażenie regularne pasuje do „tutaj” zamiast „gdziekolwiek po lastIndex”. Zobacz podany link!
Doin
1
Wstępne stwierdzenie tej odpowiedzi jest po prostu niedokładne. Podkreśliłeś krok 3 specyfikacji, który nic nie mówi. Rzeczywisty wpływ lastIndexznajduje się w krokach 5, 6 i 11. Twoje oświadczenie otwierające jest prawdziwe, JEŚLI GLOBALNA FLAGA JEST USTAWIONA.
Prestaul
@ Prestaul tak, masz rację, że nie wspomina o globalnej fladze. Było to prawdopodobnie (nie pamiętam, co wtedy myślałem) ukryte ze względu na sposób sformułowania pytania. Edytuj odpowiedź lub usuń ją i umieść link do swojej odpowiedzi. Pozwól, że upewnię cię, że jesteś lepszy ode mnie. Cieszyć się!
Ionuț G. Stan
@ IonuțG.Stan, przepraszam, jeśli mój poprzedni komentarz wydawał się atakujący, to nie był mój zamiar. W tym momencie nie mogę go edytować, ale nie próbowałem krzyczeć, tylko zwrócić uwagę na zasadniczy punkt mojego komentarza. Mój błąd!
Prestaul
72

Używasz jednego RegExpobiektu i wykonujesz go wiele razy. Przy każdym kolejnym wykonaniu jest kontynuowany od indeksu ostatniego dopasowania.

Musisz „zresetować” wyrażenie regularne, aby rozpocząć od początku przed każdym wykonaniem:

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

Powiedziawszy, że może być bardziej czytelne tworzenie nowego obiektu RegExp za każdym razem (obciążenie jest minimalne, ponieważ RegExp jest buforowany i tak):

result.push((/Foo B/gi).test(stringA));
result.push((/Foo B/gi).test(stringB));
Roatin Marth
źródło
1
Lub po prostu nie używaj gflagi.
melpomene
36

RegExp.prototype.testaktualizuje właściwość wyrażeń regularnych, lastIndexaby każdy test rozpoczął się w miejscu, w którym zatrzymał się ostatni. Sugeruję użycie, String.prototype.matchponieważ nie aktualizuje lastIndexwłaściwości:

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

Uwaga: !!konwertuje go na wartość logiczną, a następnie odwraca wartość logiczną, aby odzwierciedlała wynik.

Alternatywnie możesz po prostu zresetować lastIndexwłaściwość:

result.push(re.test('Foo Bar'));
re.lastIndex = 0;
result.push(re.test('Foo Bar'));
James
źródło
11

Usunięcie gflagi globalnej rozwiąże problem.

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

Powinien być

var re = new RegExp(query, 'i');
użytkownik2572074
źródło
0

Musisz ustawić re.lastIndex = 0, ponieważ przy regexie flagi g śledź ostatnie wystąpienie dopasowania, więc test nie przejdzie do testowania tego samego ciągu, w tym celu musisz zrobić 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)

Ashish
źródło
-1

Miałem funkcję:

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

Pierwsze połączenie działa. Drugie połączenie nie. sliceOperacja narzeka wartości zerowej. Zakładam, że dzieje się tak z powodu re.lastIndex. Jest to dziwne, ponieważ oczekiwałbym, że nowa RegExpbędzie przydzielana za każdym razem, gdy funkcja jest wywoływana, a nie współużytkowana przez wiele wywołań mojej funkcji.

Kiedy zmieniłem na:

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

Wtedy nie dostaję lastIndexefektu zatrzymania. Działa tak, jak bym tego oczekiwał.

Chełmite
źródło