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.
javascript
regex
recursivo
fuente
fuente
"asdf".match(/.*/g)
return ["asdf", ""]"aa".replace(/b*/, "b")
dar como resultadobabab
. Y en algún momento estandarizamos todos los detalles de implementación de los navegadores web.Respuestas:
Según el estándar ECMA-262 , String.prototype.replace llama a RegExp.prototype [@@ replace] , que dice:
donde
rx
es/.*/g
yS
es'asdf'
.Ver 11.c.iii.2.b:
Por
'asdf'.replace(/.*/g, 'x')
lo tanto, en realidad es:[]
, lastIndex =0
'asdf'
, resultados =[ 'asdf' ]
, lastIndex =4
''
, resultados =[ 'asdf', '' ]
, lastIndex =4
,AdvanceStringIndex
, establecen lastIndex a5
null
, resultados =[ 'asdf', '' ]
, retornoPor lo tanto hay 2 partidos.
fuente
'asdf'
y una cadena vacía''
.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
(matchStr, matchIndex)
en orden cronológico que indica qué partes e índices de la cadena de entrada ya se han comido.matchIndex
sobrescribiendo la subcadenamatchStr
en esa posición. SimatchStr = ""
, 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
"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 aparentePor 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.
"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
x
en las posiciones 0, 1, 2, 3 y 4."abcd".replace(/.+?/g, "x")
con salidas cuantificadoras diferidas+?
"xxxx"
[("a", 0), ("b", 1), ("c", 2), ("d", 3)]
"abcd".replace(/.{2,}?/g, "x")
con salidas cuantificadoras diferidas[2,}?
"xx"
[("ab", 0), ("cd", 2)]
"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í:
"abcdefgh".replace(/(?<=^(..)*)/g, "_"))
con una búsqueda hacia atrás positivos(?<=...)
salidas"_ab_cd_ef_gh_"
(sólo está soportado en Chrome hasta ahora)[("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]
"abcdefgh".replace(/(?=(..)*$)/g, "_"))
con salidas positivas anticipadas(?=...)
"_ab_cd_ef_gh_"
[("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]
fuente
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.("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)
iffix2 >= ix1 + str1.length() && ix2 + str2.length() > ix1 + str1.length()
, lo que no causa esta falla.("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!"" ⋅ ε = ""
, 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.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.fuente
simplemente, el primero
x
es para reemplazar el emparejamientoasdf
.segundo
x
para la cadena vacía despuésasdf
. La búsqueda termina cuando está vacía.fuente