¿Por qué un RegExp con bandera global da resultados incorrectos?

277

¿Cuál es el problema con esta expresión regular cuando uso el indicador global y el distintivo entre mayúsculas y minúsculas? La consulta es una entrada generada por el usuario. El resultado debe ser [verdadero, verdadero].

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

acerca de
fuente
54
Bienvenido a una de las muchas trampas de RegExp en JavaScript. Tiene una de las peores interfaces para el procesamiento de expresiones regulares que he conocido, llena de efectos secundarios extraños y advertencias oscuras. La mayoría de las tareas comunes que normalmente quieres hacer con expresiones regulares son difíciles de deletrear correctamente.
bobince 05 de
XRegExp parece una buena alternativa. xregexp.com
aproximadamente el
Vea la respuesta aquí también: stackoverflow.com/questions/604860/…
Prestaul
Una solución, si puede salirse con la suya, es usar el literal regex directamente en lugar de guardarlo en re.
Thdoan

Respuestas:

350

El RegExpobjeto realiza un seguimiento del lugar lastIndexdonde se produjo una coincidencia, por lo que en las coincidencias posteriores comenzará desde el último índice utilizado, en lugar de 0. Observe:

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

Si no desea restablecer manualmente lastIndexa 0 después de cada prueba, simplemente elimine la gbandera.

Aquí está el algoritmo que dictan las especificaciones (sección 15.10.6.2):

RegExp.prototype.exec (cadena)

Realiza una coincidencia de expresión regular de cadena contra la expresión regular y devuelve un objeto Array que contiene los resultados de la coincidencia, o nulo si la cadena no coincide La cadena ToString (cadena) busca una aparición del patrón de expresión regular de la siguiente manera:

  1. Deje que S sea el valor de ToString (cadena).
  2. Deje que la longitud sea la longitud de S.
  3. Deje lastIndex ser el valor de la propiedad lastIndex.
  4. Permítame ser el valor de ToInteger (lastIndex).
  5. Si la propiedad global es falsa, sea i = 0.
  6. Si la longitud I <0 o I> establezca lastIndex en 0 y devuelva nulo.
  7. Llama a [[Match]], dándole los argumentos S e i. Si [[Match]] devolvió el error, vaya al paso 8; de lo contrario, sea r su resultado de estado y vaya al paso 10.
  8. Sea i = i + 1.
  9. Ve al paso 6.
  10. Sea e el valor endIndex de r.
  11. Si la propiedad global es verdadera, establezca lastIndex en e.
  12. Sea n la longitud de la matriz de capturas de r. (Este es el mismo valor que NCapturingParens de 15.10.2.1).
  13. Devuelve una nueva matriz con las siguientes propiedades:
    • La propiedad de índice se establece en la posición de la subcadena coincidente dentro de la cadena completa S.
    • La propiedad de entrada se establece en S.
    • La propiedad de longitud se establece en n + 1.
    • La propiedad 0 se establece en la subcadena coincidente (es decir, la parte de S entre el desplazamiento i inclusivo y el desplazamiento e exclusivo).
    • Para cada entero i tal que I> 0 e I ≤ n, establezca la propiedad llamada ToString (i) en el elemento i-ésimo de la matriz de capturas de r.
Ionuț G. Stan
fuente
83
Esto es como la Guía del autoestopista para el diseño de la API de Galaxy aquí. "Ese escollo en el que caíste ha sido perfectamente documentado en la especificación durante varios años, si solo te hubieras molestado en comprobarlo"
Retsam
55
La bandera adhesiva de Firefox no hace lo que usted implica en absoluto. Más bien, actúa como si hubiera un ^ al comienzo de la expresión regular, EXCEPTO que este ^ coincide con la posición actual de la cadena (lastIndex) en lugar del inicio de la cadena. Está probando efectivamente si la expresión regular coincide con "aquí" en lugar de "en cualquier lugar después de lastIndex". ¡Vea el enlace que proporcionó!
Doin
1
La declaración inicial de esta respuesta simplemente no es precisa. Destacó el paso 3 de la especificación que no dice nada. La influencia real de lastIndexestá en los pasos 5, 6 y 11. Su declaración de apertura solo es cierta SI LA BANDERA GLOBAL ESTÁ CONFIGURADA.
Prestaul
@Prestaul sí, tienes razón en que no menciona la bandera global. Probablemente fue (no recuerdo lo que pensaba entonces) implícito debido a la forma en que se formula la pregunta. Siéntase libre de editar la respuesta o eliminarla y vincularla a su respuesta. Además, déjame asegurarte que eres mejor que yo. ¡Disfrutar!
Ionuț G. Stan
@ IonuțG.Stan, perdón si mi comentario anterior parecía atacante, esa no era mi intención. No puedo editarlo en este momento, pero no estaba tratando de gritar, solo para llamar la atención sobre el punto esencial de mi comentario. ¡Culpa mía!
Prestaul
72

Está utilizando un solo RegExpobjeto y ejecutándolo varias veces. En cada ejecución sucesiva continúa desde el último índice de coincidencia.

Debe "restablecer" la expresión regular para comenzar desde el principio antes de cada ejecución:

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

Dicho esto, puede ser más legible crear un nuevo objeto RegExp cada vez (la sobrecarga es mínima ya que RegExp se almacena en caché de todos modos):

result.push((/Foo B/gi).test(stringA));
result.push((/Foo B/gi).test(stringB));
Roatin Marth
fuente
1
O simplemente no use la gbandera.
melpomene
36

RegExp.prototype.testactualiza la lastIndexpropiedad de las expresiones regulares para que cada prueba comience donde se detuvo la última. Sugeriría usar String.prototype.matchya que no actualiza la lastIndexpropiedad:

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

Nota: !! convierte en booleano y luego lo invierte para que refleje el resultado.

Alternativamente, puede restablecer la lastIndexpropiedad:

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

Eliminar la gbandera global solucionará su problema.

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

Debiera ser

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

Debe establecer re.lastIndex = 0 porque con g flag regex realiza un seguimiento de la última coincidencia ocurrida, por lo que la prueba no irá a probar la misma cadena, para eso debe hacer 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
fuente
-1

Tenía la función:

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 primera llamada funciona. La segunda llamada no. La sliceoperación se queja de un valor nulo. Supongo que esto se debe a la re.lastIndex. Esto es extraño porque esperaría un nuevoRegExp que se asigne cada vez que se llama a la función y no se comparte entre múltiples invocaciones de mi función.

Cuando lo cambié a:

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

Entonces no obtengo el lastIndexefecto remanente. Funciona como lo esperaría.

Chelmite
fuente