Perché un RegExp con flag globale fornisce risultati errati?


277

Qual è il problema con questa espressione regolare quando uso la bandiera globale e la bandiera insensibile al maiuscolo / minuscolo? La query è un input generato dall'utente. Il risultato dovrebbe essere [vero, vero].

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
Benvenuti in una delle tante trappole di RegExp in JavaScript. Ha una delle peggiori interfacce per l'elaborazione del regex che abbia mai incontrato, piena di strani effetti collaterali e oscuri avvertimenti. La maggior parte delle attività comuni che in genere si desidera svolgere con regex sono difficili da pronunciare correttamente.
bobince,

XRegExp sembra una buona alternativa. xregexp.com
circa

Si veda la risposta anche qui: stackoverflow.com/questions/604860/...
Prestaul

Una soluzione, se riesci a cavartela, è usare direttamente il regex letterale invece di salvarlo re.
entro il

Risposte:


350

L' RegExpoggetto tiene traccia del punto in lastIndexcui si è verificata una corrispondenza, quindi nelle corrispondenze successive inizierà dall'ultimo indice utilizzato, anziché 0. Dai un'occhiata:

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

Se non si desidera reimpostare manualmente lastIndexsu 0 dopo ogni test, è sufficiente rimuovere il gflag.

Ecco l'algoritmo dettato dalle specifiche (sezione 15.10.6.2):

RegExp.prototype.exec (stringa)

Esegue una corrispondenza di espressione regolare di stringa rispetto all'espressione regolare e restituisce un oggetto Array contenente i risultati della corrispondenza oppure null se la stringa non corrisponde. La stringa ToString (stringa) viene cercata per un'occorrenza del modello di espressione regolare come segue:

  1. Sia S il valore di ToString (stringa).
  2. Lascia che la lunghezza sia la lunghezza di S.
  3. Sia lastIndex il valore della proprietà lastIndex.
  4. Lascia che io sia il valore di ToInteger (lastIndex).
  5. Se la proprietà globale è falsa, lasciare i = 0.
  6. Se I <0 o I> lunghezza, imposta lastIndex su 0 e restituisce null.
  7. Chiama [[Match]], dandogli gli argomenti S e i. Se [[Match]] ha restituito un errore, andare al passaggio 8; altrimenti sia r il risultato dello stato e vai al passaggio 10.
  8. Lascia che i = i + 1.
  9. Vai al passaggio 6.
  10. Sia e sia il valore endIndex di r.
  11. Se la proprietà globale è vera, impostare lastIndex su e.
  12. Sia n la lunghezza dell'array di acquisizioni di r. (Questo è lo stesso valore di NCapturingParens del 15.10.2.1.)
  13. Restituisce un nuovo array con le seguenti proprietà:
    • La proprietà index è impostata sulla posizione della sottostringa corrispondente all'interno della stringa completa S.
    • La proprietà di input è impostata su S.
    • La proprietà length è impostata su n + 1.
    • La proprietà 0 è impostata sulla sottostringa corrispondente (ovvero la parte di S tra offset i compreso e offset e esclusivo).
    • Per ogni intero i tale che I> 0 e I ≤ n, impostare la proprietà denominata ToString (i) sull'elemento ith dell'array di acquisizioni di r.

83
Questo è come Hitchhiker's Guide to the Galaxy API design here. "Quel trabocchetto in cui ti sei imbattuto è stato perfettamente documentato nelle specifiche per diversi anni, se solo ti fossi preso la briga di controllare"
Retsam,

5
La bandiera adesiva di Firefox non fa affatto ciò che implica. Piuttosto, si comporta come se ci fosse un ^ all'inizio dell'espressione regolare, TRANNE che questo ^ corrisponde alla posizione della stringa corrente (lastIndex) piuttosto che all'inizio della stringa. Stai effettivamente testando se il regex corrisponde a "proprio qui" anziché "ovunque dopo lastIndex". Vedi il link che hai fornito!
Doin,

1
La dichiarazione di apertura di questa risposta non è precisa. Hai evidenziato il passaggio 3 delle specifiche che non dice nulla. L'effettiva influenza di lastIndexè nei passaggi 5, 6 e 11. La tua dichiarazione di apertura è vera solo SE IL FLAG GLOBALE È IMPOSTATO.
Prestaul,

@Prestaul sì, hai ragione che non menziona la bandiera globale. Probabilmente era (non ricordo cosa pensavo allora) implicito a causa del modo in cui la domanda è inquadrata. Sentiti libero di modificare la risposta o di eliminarla e di collegarti alla tua risposta. Inoltre, lascia che ti rassicuri sul fatto che sei migliore di me. Godere!
Ionuț G. Stan,

@ IonuțG.Stan, scusami se il mio commento precedente sembrava aggressivo, non era questo il mio intento. Non posso modificarlo a questo punto, ma non stavo cercando di urlare, solo per attirare l'attenzione sul punto essenziale del mio commento. Colpa mia!
Prestaul,

72

Stai utilizzando un singolo RegExpoggetto e lo stai eseguendo più volte. Ad ogni successiva esecuzione continua dall'ultimo indice di corrispondenza.

Devi "resettare" il regex per iniziare dall'inizio prima di ogni esecuzione:

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

Detto questo, potrebbe essere più leggibile creare un nuovo oggetto RegExp ogni volta (l'overhead è minimo poiché RegExp viene comunque memorizzato nella cache):

result.push((/Foo B/gi).test(stringA));
result.push((/Foo B/gi).test(stringB));

1
O semplicemente non usare la gbandiera.
melpomene,

36

RegExp.prototype.testaggiorna la lastIndexproprietà delle espressioni regolari in modo che ciascun test inizi nel punto in cui si è interrotto l'ultimo. Suggerirei di usare String.prototype.matchpoiché non aggiorna la lastIndexproprietà:

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

Nota: !! converte in un valore booleano e quindi inverte il valore booleano in modo che rifletta il risultato.

In alternativa, puoi semplicemente ripristinare la lastIndexproprietà:

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

12

La rimozione della gbandiera globale risolverà il tuo problema.

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

Dovrebbe essere

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


0

Devi impostare re.lastIndex = 0 perché con g flag regex tieni traccia dell'ultima corrispondenza avvenuta, quindi test non andrà a testare la stessa stringa, per questo devi fare 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

Ho avuto la funzione:

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

La prima chiamata funziona. La seconda chiamata no. L' sliceoperazione lamenta un valore nullo. Presumo che ciò sia dovuto al re.lastIndex. Questo è strano perché mi aspetterei un nuovoRegExp venga assegnato ogni volta che la funzione viene chiamata e non condivisa tra più invocazioni della mia funzione.

Quando l'ho cambiato in:

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

Quindi non ottengo l' lastIndexeffetto holdover. Funziona come mi aspetterei.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.