Eu me deparei com um fato surpreendente (para mim).
console.log("asdf".replace(/.*/g, "x"));
Por que duas substituições? Parece que qualquer sequência não vazia sem novas linhas produzirá exatamente duas substituições para esse padrão. Usando uma função de substituição, posso ver que a primeira substituição é para toda a cadeia e a segunda é para uma cadeia vazia.
javascript
regex
recursivo
fonte
fonte
"asdf".match(/.*/g)
return ["asdf", ""]"aa".replace(/b*/, "b")
de resultarbabab
. E em algum momento padronizamos todos os detalhes de implementação dos navegadores da web.Respostas:
De acordo com o padrão ECMA-262 , String.prototype.replace chama RegExp.prototype [@@ replace] , que diz:
onde
rx
está/.*/g
eS
está'asdf'
.Ver 11.c.iii.2.b:
Portanto, na
'asdf'.replace(/.*/g, 'x')
verdade é:[]
, lastIndex =0
'asdf'
, resultados =[ 'asdf' ]
, lastIndex =4
''
, = resultados[ 'asdf', '' ]
, lastIndex =4
,AdvanceStringIndex
, ajustado para lastIndex5
null
, resultados =[ 'asdf', '' ]
, retornoPortanto, existem 2 correspondências.
fonte
'asdf'
string vazia''
.Juntos, em um bate-papo offline com o yawkat , encontramos uma maneira intuitiva de ver por que
"abcd".replace(/.*/g, "x")
exatamente produz duas correspondências. Observe que não verificamos se é totalmente igual à semântica imposta pelo padrão ECMAScript, portanto, tome isso como uma regra geral.Regras de ouro
(matchStr, matchIndex)
em ordem cronológica que indica quais partes e índices da string de entrada já foram consumidos.matchIndex
substituindo a substringmatchStr
nessa posição. SematchStr = ""
, então a "substituição" é efetivamente inserção.Formalmente, o ato de correspondência e substituição é descrito como um loop, como visto na outra resposta .
Exemplos fáceis
"abcd".replace(/.*/g, "x")
saídas"xx"
:A lista de correspondências é
[("abcd", 0), ("", 4)]
Notavelmente, ele não inclui as seguintes correspondências pelas quais se poderia pensar pelas seguintes razões:
("a", 0)
,("ab", 0)
: o quantificador*
é ganancioso("b", 1)
,("bc", 1)
: devido à partida anterior("abcd", 0)
, as cordas"b"
e"bc"
já estão comidas("", 4), ("", 4)
(ou seja, duas vezes): a posição 4 do índice já foi consumida pela primeira correspondência aparentePortanto, a cadeia de
"x"
substituição substitui as cadeias de correspondência encontradas exatamente nessas posições: na posição 0 substitui a cadeia"abcd"
e na posição 4 substitui""
.Aqui você pode ver que a substituição pode atuar como uma verdadeira substituição de uma string anterior ou apenas como a inserção de uma nova string.
"abcd".replace(/.*?/g, "x")
com saídas de um quantificador preguiçoso*?
"xaxbxcxdx"
A lista de correspondências é
[("", 0), ("", 1), ("", 2), ("", 3), ("", 4)]
Em contraste com o exemplo anterior, aqui
("a", 0)
,("ab", 0)
,("abc", 0)
, ou mesmo("abcd", 0)
não estão incluídos devido à preguiça do quantificador que estritamente limita-lo para encontrar a correspondência mais curta possível.Como todas as seqüências de correspondência estão vazias, nenhuma substituição real ocorre, mas inserções
x
nas posições 0, 1, 2, 3 e 4."abcd".replace(/.+?/g, "x")
com saídas de um quantificador preguiçoso+?
"xxxx"
[("a", 0), ("b", 1), ("c", 2), ("d", 3)]
"abcd".replace(/.{2,}?/g, "x")
com saídas de um quantificador preguiçoso[2,}?
"xx"
[("ab", 0), ("cd", 2)]
"abcd".replace(/.{0}/g, "x")
saídas"xaxbxcxdx"
pela mesma lógica do exemplo 2.Exemplos mais difíceis
Podemos explorar consistentemente a ideia de inserção em vez de substituição, se sempre correspondermos a uma string vazia e controlar a posição em que essas correspondências acontecem em nossa vantagem. Por exemplo, podemos criar expressões regulares correspondentes à string vazia em todas as posições pares para inserir um caractere:
"abcdefgh".replace(/(?<=^(..)*)/g, "_"))
com um lookbehind positivos(?<=...)
saídas"_ab_cd_ef_gh_"
(somente suportadas em cromo até agora)[("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]
"abcdefgh".replace(/(?=(..)*$)/g, "_"))
com saídas positivas à frente(?=...)
"_ab_cd_ef_gh_"
[("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]
fonte
while (!input not eaten up) { matchAndEat(); }
. Além disso, os comentários acima indicam que o comportamento se originou há muito tempo antes da existência do JavaScript.("abcd", 0)
não ocupa a posição 4 onde o personagem seguinte iria, mas a correspondência de zero caracteres("", 4)
não coma a posição 4 onde o personagem a seguir iria. Se eu estivesse projetando isso do zero, acho que a regra que eu usaria é a que(str2, ix2)
segue o(str1, ix1)
iffix2 >= ix1 + str1.length() && ix2 + str2.length() > ix1 + str1.length()
, o que não causa esse erro.("abcd", 0)
não ocupa a posição 4, pois"abcd"
possui apenas 4 caracteres e, portanto, apenas come os índices 0, 1, 2, 3. Posso ver de onde seu raciocínio pode vir: por que não podemos ter("abcd" ⋅ ε, 0)
uma correspondência de 5 caracteres em que ⋅ é concatenação eε
a correspondência de largura zero? Formalmente porque"abcd" ⋅ ε = "abcd"
. Pensei em uma razão intuitiva para os últimos minutos, mas não consegui encontrar uma. Eu acho que é preciso sempre tratarε
como apenas ocorrendo por conta própria""
. Eu adoraria jogar com uma implementação alternativa sem esse bug ou façanha. Sinta-se à vontade para compartilhar!"" ⋅ ε = ""
, embora eu não tenha certeza de qual distinção você pretende fazer""
eε
que significa a mesma coisa). Portanto, a diferença não pode ser explicada como intuitiva - é simplesmente.A primeira partida é obviamente
"asdf"
(posição [0,4]). Como o sinalizador global (g
) está definido, ele continua pesquisando. Nesse ponto (posição 4), ele encontra uma segunda correspondência, uma string vazia (posição [4,4]).Lembre-se de que
*
corresponde a zero ou mais elementos.fonte
simplesmente, o primeiro
x
é para a substituição da correspondênciaasdf
.segundo
x
para a sequência vazia depoisasdf
. A pesquisa termina quando vazia.fonte