¿Por qué es "asdf" .replace (/.*/ g, "x") == "xx"?

132

Me topé con un hecho sorprendente (para mí).

console.log("asdf".replace(/.*/g, "x"));

¿Por qué dos reemplazos? Parece que cualquier cadena no vacía sin líneas nuevas producirá exactamente dos reemplazos para este patrón. Usando una función de reemplazo, puedo ver que el primer reemplazo es para toda la cadena, y el segundo es para una cadena vacía.

recursivo
fuente
99
ejemplo más simple: "asdf".match(/.*/g)return ["asdf", ""]
Narro
32
Debido a la bandera global (g). El indicador global permite que otra búsqueda comience al final de la coincidencia anterior, encontrando así una cadena vacía.
Celsiuss
66
y seamos honestos: probablemente nadie quería exactamente ese comportamiento. probablemente fue un detalle de implementación de querer "aa".replace(/b*/, "b")dar como resultado babab. Y en algún momento estandarizamos todos los detalles de implementación de los navegadores web.
Lux
44
@Joshua versiones anteriores de GNU sed (¡no otras implementaciones!) También exhibían este error, que se solucionó en algún lugar entre las versiones 2.05 y 3.01 (hace más de 20 años). Sospecho que es allí donde se originó este comportamiento, antes de llegar a Perl (donde se convirtió en una característica) y de ahí a JavaScript.
mosvy
1
@recursive - Bastante justo. Los encuentro sorprendentes por un segundo, luego me doy cuenta de "coincidencia de ancho cero" y ya no me sorprende. :-)
TJ Crowder

Respuestas:

98

Según el estándar ECMA-262 , String.prototype.replace llama a RegExp.prototype [@@ replace] , que dice:

11. Repeat, while done is false
  a. Let result be ? RegExpExec(rx, S).
  b. If result is null, set done to true.
  c. Else result is not null,
    i. Append result to the end of results.
    ii. If global is false, set done to true.
    iii. Else,
      1. Let matchStr be ? ToString(? Get(result, "0")).
      2. If matchStr is the empty String, then
        a. Let thisIndex be ? ToLength(? Get(rx, "lastIndex")).
        b. Let nextIndex be AdvanceStringIndex(S, thisIndex, fullUnicode).
        c. Perform ? Set(rx, "lastIndex", nextIndex, true).

donde rxes /.*/gy Ses 'asdf'.

Ver 11.c.iii.2.b:

si. Deje que nextIndex sea AdvanceStringIndex (S, thisIndex, fullUnicode).

Por 'asdf'.replace(/.*/g, 'x')lo tanto, en realidad es:

  1. resultado (indefinido), resultados = [], lastIndex =0
  2. resultado = 'asdf', resultados = [ 'asdf' ], lastIndex =4
  3. resultado = '', resultados = [ 'asdf', '' ], lastIndex = 4, AdvanceStringIndex, establecen lastIndex a5
  4. resultado = null, resultados = [ 'asdf', '' ], retorno

Por lo tanto hay 2 partidos.

Alan Liang
fuente
42
Esta respuesta requiere que lo estudie para entenderlo.
Felipe
El TL; DR es que coincide 'asdf'y una cadena vacía ''.
Jim
34

Juntos en un chat sin conexión con yawkat , encontramos una forma intuitiva de ver por qué "abcd".replace(/.*/g, "x")exactamente produce dos coincidencias. Tenga en cuenta que no hemos comprobado si es completamente igual a la semántica impuesta por el estándar ECMAScript, por lo tanto, solo tómelo como una regla general.

Reglas de juego

  • Considere las coincidencias como una lista de tuplas (matchStr, matchIndex)en orden cronológico que indica qué partes e índices de la cadena de entrada ya se han comido.
  • Esta lista se construye continuamente a partir de la izquierda de la cadena de entrada para la expresión regular.
  • Las piezas ya consumidas ya no se pueden combinar
  • El reemplazo se realiza en los índices dados matchIndexsobrescribiendo la subcadena matchStren esa posición. Si matchStr = "", entonces el "reemplazo" es efectivamente la inserción.

Formalmente, el acto de emparejamiento y reemplazo se describe como un ciclo como se ve en la otra respuesta .

Ejemplos fáciles

  1. "abcd".replace(/.*/g, "x")salidas "xx":

    • La lista de coincidencias es [("abcd", 0), ("", 4)]

      Notablemente, no incluye las siguientes coincidencias en las que uno podría haber pensado por las siguientes razones:

      • ("a", 0), ("ab", 0): el cuantificador *es codicioso
      • ("b", 1), ("bc", 1): debido al partido anterior ("abcd", 0), las cuerdas "b"y "bc"ya están carcomidas
      • ("", 4), ("", 4) (es decir, dos veces): la posición de índice 4 ya está carcomida por la primera coincidencia aparente
    • Por lo tanto, la cadena de reemplazo "x"reemplaza las cadenas de coincidencia encontradas exactamente en esas posiciones: en la posición 0 reemplaza la cadena "abcd"y en la posición 4 reemplaza "".

      Aquí puede ver que el reemplazo puede actuar como un reemplazo verdadero de una cadena anterior o simplemente como la inserción de una nueva cadena.

  2. "abcd".replace(/.*?/g, "x")con salidas cuantificadoras diferidas*?"xaxbxcxdx"

    • La lista de coincidencias es [("", 0), ("", 1), ("", 2), ("", 3), ("", 4)]

      En contraste con el ejemplo anterior, aquí ("a", 0), ("ab", 0), ("abc", 0), o incluso ("abcd", 0)no se incluyen debido a la pereza del cuantificador que se limita estrictamente a encontrar la coincidencia más corta posible.

    • Como todas las cadenas de coincidencia están vacías, no se produce un reemplazo real, sino que se insertan xen las posiciones 0, 1, 2, 3 y 4.

  3. "abcd".replace(/.+?/g, "x")con salidas cuantificadoras diferidas+?"xxxx"

    • La lista de coincidencias es [("a", 0), ("b", 1), ("c", 2), ("d", 3)]
  4. "abcd".replace(/.{2,}?/g, "x")con salidas cuantificadoras diferidas[2,}?"xx"

    • La lista de coincidencias es [("ab", 0), ("cd", 2)]
  5. "abcd".replace(/.{0}/g, "x")salidas "xaxbxcxdx"por la misma lógica que en el ejemplo 2.

Ejemplos más difíciles

Podemos explotar consistentemente la idea de inserción en lugar de reemplazo si solo hacemos coincidir una cadena vacía y controlamos la posición donde tales coincidencias suceden para nuestra ventaja. Por ejemplo, podemos crear expresiones regulares que coincidan con la cadena vacía en cada posición par para insertar un carácter allí:

  1. "abcdefgh".replace(/(?<=^(..)*)/g, "_"))con una búsqueda hacia atrás positivos(?<=...) salidas "_ab_cd_ef_gh_"(sólo está soportado en Chrome hasta ahora)

    • La lista de coincidencias es [("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]
  2. "abcdefgh".replace(/(?=(..)*$)/g, "_"))con salidas positivas anticipadas(?=...)"_ab_cd_ef_gh_"

    • La lista de coincidencias es [("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]
ComFreek
fuente
44
Creo que es un poco exagerado llamarlo intuitivo (y en negrita). Para mí, se parece más al síndrome de Estocolmo y la racionalización post-hoc. Su respuesta es buena, por cierto, solo me quejo del diseño de JS o de la falta de diseño.
Eric Duminil
77
@EricDuminil Yo también lo pensé al principio, pero después de haber escrito la respuesta, el algoritmo global de reemplazo de expresiones regulares parece ser exactamente la forma en que uno pensaría si comenzara desde cero. Es como while (!input not eaten up) { matchAndEat(); }. Además, los comentarios anteriores indican que el comportamiento se originó hace mucho tiempo antes de la existencia de JavaScript.
ComFreek
2
La parte que todavía no tiene sentido (por cualquier otra razón que no sea "eso es lo que dice el estándar") es que la coincidencia de cuatro caracteres ("abcd", 0)no se come la posición 4 donde iría el siguiente personaje, pero la coincidencia de cero caracteres ("", 4)sí comer la posición 4 donde iría el siguiente personaje. Si estuviera diseñando esto desde cero, creo que la regla que usaría es que (str2, ix2)puede seguir (str1, ix1)iff ix2 >= ix1 + str1.length() && ix2 + str2.length() > ix1 + str1.length(), lo que no causa esta falla.
Anders Kaseorg
2
@AndersKaseorg ("abcd", 0)no come la posición 4 porque "abcd"tiene solo 4 caracteres y, por lo tanto, solo come los índices 0, 1, 2, 3. Puedo ver de dónde puede venir su razonamiento: ¿por qué no podemos tener ("abcd" ⋅ ε, 0)una coincidencia de 5 caracteres de largo donde where Qué es la concatenación y εla coincidencia de ancho cero? Formalmente porque "abcd" ⋅ ε = "abcd". Pensé en una razón intuitiva para los últimos minutos, pero no pude encontrar una. Supongo que uno siempre debe tratar εcomo algo que ocurre por sí solo "". Me encantaría jugar con una implementación alternativa sin ese error o hazaña. ¡No dudes en compartir!
ComFreek
1
Si la cadena de cuatro caracteres debe comer cuatro índices, entonces la cadena de caracteres cero no debe comer índices. Cualquier razonamiento que pueda hacer sobre uno debería aplicarse igualmente al otro (por ejemplo "" ⋅ ε = "", aunque no estoy seguro de qué distinción pretende establecer entre ""y ε, lo que significa lo mismo). Por lo tanto, la diferencia no puede explicarse como intuitiva, simplemente lo es.
Anders Kaseorg
26

El primer partido es obviamente "asdf"(Posición [0,4]). Debido a que la bandera global ( g) está configurada, continúa buscando. En este punto (Posición 4), encuentra una segunda coincidencia, una cadena vacía (Posición [4,4]).

Recuerde que *coincide con cero o más elementos.

David SK
fuente
44
Entonces, ¿por qué no tres partidos? Podría haber otra cerilla vacía al final. Hay precisamente dos. Esta explicación explica por qué podría haber dos, pero no por qué debería haber en lugar de uno o tres.
recursivo
77
No, no hay otra cadena vacía. Porque se ha encontrado esa cadena vacía. una cadena vacía en la posición 4,4, se detecta como un resultado único. Una coincidencia etiquetada "4,4" no se puede repetir. probablemente pueda pensar que hay una cadena vacía en la posición [0,0] pero el operador * devuelve el máximo de elementos posible. esta es la razón por la que solo 4,4 es posible
David SK
16
Tenemos que recordar que las expresiones regulares no son expresiones regulares. En las expresiones regulares, hay infinitas cadenas vacías entre cada dos caracteres, así como al principio y al final. En las expresiones regulares, hay exactamente tantas cadenas vacías como la especificación para el sabor particular del motor de expresiones regulares dice que hay.
Jörg W Mittag
77
Esto es solo racionalización post-hoc.
mosvy
99
@mosvy excepto que es la lógica exacta la que realmente se usa.
hobbs
1

simplemente, el primero xes para reemplazar el emparejamiento asdf.

segundo xpara la cadena vacía después asdf. La búsqueda termina cuando está vacía.

Nilanka Manoj
fuente