Perché "asdf" .replace (/.*/ g, "x") == "xx"?


132

Mi sono imbattuto in un fatto sorprendente (per me).

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

Perché due sostituzioni? Sembra che qualsiasi stringa non vuota senza newline produrrà esattamente due sostituzioni per questo modello. Usando una funzione di sostituzione, posso vedere che la prima sostituzione è per l'intera stringa e la seconda per una stringa vuota.


9
esempio più semplice: "asdf".match(/.*/g)return ["asdf", ""]
Narro

32
A causa della bandiera globale (g). Il flag globale consente di iniziare un'altra ricerca alla fine della corrispondenza precedente, trovando così una stringa vuota.
Celsiuss,

6
e siamo onesti: probabilmente nessuno voleva esattamente quel comportamento. era probabilmente un dettaglio di implementazione del voler "aa".replace(/b*/, "b")ottenere babab. E ad un certo punto abbiamo standardizzato tutti i dettagli di implementazione dei browser web.
Lux

4
@Joshua versioni precedenti di GNU sed (non altre implementazioni!) Mostravano anche questo bug, che è stato corretto da qualche parte tra le versioni 2.05 e 3.01 (oltre 20 anni fa). Sospetto che sia lì che ha avuto origine questo comportamento, prima di farsi strada in perl (dove è diventato una funzionalità) e da lì in javascript.
mosvy,

1
@recursive - Abbastanza giusto. Li trovo entrambi sorprendenti per un secondo, poi realizzo una "corrispondenza a larghezza zero" e non ne sono più sorpreso. :-)
TJ Crowder,

Risposte:


98

Secondo lo standard ECMA-262 , String.prototype.replace chiama RegExp.prototype [@@ sostituisci] , che 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).

dove rxè /.*/ged Sè'asdf' .

Vedi 11.c.iii.2.b:

b. Lascia che nextIndex sia AdvanceStringIndex (S, thisIndex, fullUnicode).

Quindi in 'asdf'.replace(/.*/g, 'x')esso è in realtà:

  1. risultato (non definito), results = [], lastIndex =0
  2. risultato = 'asdf', risultati = [ 'asdf' ], lastIndex =4
  3. risultato = ''risultati = [ 'asdf', '' ], lastIndex = 4, AdvanceStringIndex, impostare lastIndex5
  4. risultato = null, risultati = [ 'asdf', '' ], ritorno

Pertanto ci sono 2 partite.


42
Questa risposta mi richiede di studiarla per capirla.
Felipe,

TL; DR è che corrisponde 'asdf'e stringa vuota ''.
jimh

34

Insieme in una chat offline con yawkat , abbiamo trovato un modo intuitivo per capire il perché"abcd".replace(/.*/g, "x") produce esattamente due corrispondenze. Nota che non abbiamo verificato se è completamente uguale alla semantica imposta dallo standard ECMAScript, quindi prendila come regola pratica.

Regole empiriche

  • Considera le corrispondenze come un elenco di tuple (matchStr, matchIndex)in ordine cronologico che indicano quali parti di stringa e indici della stringa di input sono già stati divorati.
  • Questo elenco viene continuamente compilato a partire dalla sinistra della stringa di input per il regex.
  • Le parti già consumate non possono più essere abbinate
  • La sostituzione avviene agli indici dati matchIndexsovrascrivendo la sottostringa matchStrin quella posizione. Se matchStr = "", quindi la "sostituzione" è effettivamente l'inserimento.

Formalmente, l'atto di abbinamento e sostituzione è descritto come un ciclo come visto nell'altra risposta .

Semplici esempi

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

    • L'elenco delle partite è [("abcd", 0), ("", 4)]

      In particolare, non include le seguenti corrispondenze che si potrebbero pensare per i seguenti motivi:

      • ("a", 0), ("ab", 0): il quantificatore *è avido
      • ("b", 1), ("bc", 1): a causa della partita precedente ("abcd", 0), le stringhe "b"e "bc"sono già state consumate
      • ("", 4), ("", 4) (cioè due volte): la posizione dell'indice 4 è già stata divorata dalla prima corrispondenza apparente
    • Pertanto, la stringa di sostituzione "x"sostituisce esattamente le stringhe di corrispondenza trovate in quelle posizioni: nella posizione 0 sostituisce la stringa "abcd"e nella posizione 4 sostituisce "".

      Qui puoi vedere che la sostituzione può agire come vera sostituzione di una stringa precedente o semplicemente come inserimento di una nuova stringa.

  2. "abcd".replace(/.*?/g, "x")con un output quantificatore pigro*?"xaxbxcxdx"

    • L'elenco delle partite è [("", 0), ("", 1), ("", 2), ("", 3), ("", 4)]

      In contrasto con l'esempio precedente, qui ("a", 0), ("ab", 0), ("abc", 0), o addirittura ("abcd", 0)non sono inclusi per pigrizia del quantificatore che limita rigorosamente esso per trovare l'occorrenza più breve possibile.

    • Poiché tutte le stringhe di corrispondenza sono vuote, non si verifica alcuna sostituzione effettiva, ma invece inserimenti xnelle posizioni 0, 1, 2, 3 e 4.

  3. "abcd".replace(/.+?/g, "x")con un output quantificatore pigro+?"xxxx"

    • L'elenco delle partite è [("a", 0), ("b", 1), ("c", 2), ("d", 3)]
  4. "abcd".replace(/.{2,}?/g, "x")con un output quantificatore pigro[2,}?"xx"

    • L'elenco delle partite è [("ab", 0), ("cd", 2)]
  5. "abcd".replace(/.{0}/g, "x")uscite "xaxbxcxdx"con la stessa logica dell'esempio 2.

Esempi più difficili

Possiamo sfruttare costantemente l'idea di inserimento anziché di sostituzione se abbiniamo sempre una stringa vuota e controlliamo la posizione in cui tali corrispondenze avvengono a nostro vantaggio. Ad esempio, possiamo creare espressioni regolari corrispondenti alla stringa vuota in ogni posizione pari per inserire un carattere lì:

  1. "abcdefgh".replace(/(?<=^(..)*)/g, "_"))con output lookbehind positivi(?<=...)"_ab_cd_ef_gh_" (finora supportati solo in Chrome)

    • L'elenco delle partite è [("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]
  2. "abcdefgh".replace(/(?=(..)*$)/g, "_"))con uscite lookahead positive(?=...)"_ab_cd_ef_gh_"

    • L'elenco delle partite è [("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]

4
Penso che sia un po 'complicato definirlo intuitivo (e in grassetto, a quello). A me sembra più la sindrome di Stoccolma e la razionalizzazione post-hoc. La tua risposta è buona, BTW, mi lamento solo del design JS o della mancanza di design per quella materia.
Eric Duminil,

7
@EricDuminil L'ho pensato anch'io all'inizio, ma dopo aver scritto la risposta, l'algoritmo abbozzato di sostituzione globale regex sembra essere esattamente il modo in cui uno se ne sarebbe uscito da zero. È come while (!input not eaten up) { matchAndEat(); }. Inoltre, i commenti sopra indicano che il comportamento ha avuto origine molto tempo fa prima dell'esistenza di JavaScript.
ComFreek,

2
La parte che non ha ancora senso (per qualsiasi altro motivo diverso da "questo è quello che dice lo standard") è che la corrispondenza di quattro caratteri ("abcd", 0)non mangia la posizione 4 in cui andrebbe il personaggio seguente, ma la corrispondenza di zero caratteri lo ("", 4)fa mangia la posizione 4 dove andrebbe il seguente personaggio. Se stavo progettando questo da zero, penso che la regola che userei è che (str2, ix2)potrebbe seguire (str1, ix1)iff ix2 >= ix1 + str1.length() && ix2 + str2.length() > ix1 + str1.length(), il che non causa questo malfunzionamento.
Anders Kaseorg,

2
@AndersKaseorg ("abcd", 0)non mangia la posizione 4 perché"abcd" è lunga solo 4 caratteri e quindi mangia solo gli indici 0, 1, 2, 3. Posso vedere da dove potrebbe venire il tuo ragionamento: perché non possiamo avere("abcd" ⋅ ε, 0) una corrispondenza di 5 caratteri in cui ⋅ la concatenazione e εla larghezza zero corrispondono? Formalmente perché "abcd" ⋅ ε = "abcd". Ho pensato a un motivo intuitivo per gli ultimi minuti, ma non sono riuscito a trovarne uno. Immagino che uno debba sempre trattare εcome se si verificasse da solo "". Mi piacerebbe giocare con un'implementazione alternativa senza quel bug o impresa. Sentiti libero di condividere!
ComFreek,

1
Se la stringa di quattro caratteri dovrebbe mangiare quattro indici, la stringa di caratteri zero non dovrebbe mangiare alcun indice. Qualsiasi ragionamento che potresti fare sull'uno dovrebbe valere anche per l'altro (ad esempio "" ⋅ ε = "", anche se non sono sicuro di quale distinzione intendi tracciare tra , ""e εche significhi la stessa cosa). Quindi la differenza non può essere spiegata come intuitiva, semplicemente lo è.
Anders Kaseorg,

26

La prima partita è ovviamente "asdf"(Posizione [0,4]). Poiché il flag globale ( g) è impostato, continua la ricerca. A questo punto (Posizione 4), trova una seconda corrispondenza, una stringa vuota (Posizione [4,4]).

Ricorda che *corrisponde a zero o più elementi.


4
Quindi perché non tre partite? Potrebbe esserci un'altra partita vuota alla fine. Ce ne sono esattamente due. Questa spiegazione spiega perché potrebbero essercene due, ma non perché dovrebbero essercene invece uno o tre.
ricorsivo il

7
No, non ci sono altre stringhe vuote. Perché quella stringa vuota è stata trovata. una stringa vuota nella posizione 4,4, viene rilevata come risultato unico. Una partita etichettata "4,4" non può essere ripetuta. probabilmente puoi pensare che ci sia una stringa vuota nella posizione [0,0] ma l'operatore * restituisce il massimo possibile di elementi. questo è il motivo per cui è possibile solo 4,4
David SK

16
Dobbiamo ricordare che le regex non sono espressioni regolari. Nelle espressioni regolari, ci sono infinite stringhe vuote tra ogni due caratteri, così come all'inizio e alla fine. Nei regex, ci sono esattamente tante stringhe vuote quante sono le specifiche per il particolare sapore del motore regex.
Jörg W Mittag,

7
Questa è solo razionalizzazione post-hoc.
mosvy

9
@mosvy tranne che è la logica esatta che viene effettivamente utilizzata.
Hobbs

1

semplicemente, il primo x è per la sostituzione della corrispondenza asdf.

secondo xper la stringa vuota dopo asdf. La ricerca termina quando è vuota.

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.