Warum ist "asdf" .replace (/.*/ g, "x") == "xx"?

131

Ich stolperte über eine überraschende (für mich) Tatsache.

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

Warum zwei Ersetzungen? Es scheint, dass jede nicht leere Zeichenfolge ohne Zeilenumbrüche genau zwei Ersetzungen für dieses Muster erzeugt. Mit einer Ersetzungsfunktion kann ich sehen, dass die erste Ersetzung für die gesamte Zeichenfolge und die zweite für eine leere Zeichenfolge gilt.

rekursiv
quelle
9
einfacheres Beispiel: "asdf".match(/.*/g)return ["asdf", ""]
Narro
32
Wegen des globalen (g) Flags. Das globale Flag ermöglicht, dass eine weitere Suche am Ende der vorherigen Übereinstimmung beginnt und somit eine leere Zeichenfolge gefunden wird.
Celsiuss
6
und seien wir ehrlich: wahrscheinlich wollte niemand genau dieses Verhalten. Es war wahrscheinlich ein Implementierungsdetail, "aa".replace(/b*/, "b")zu dem man führen wollte babab. Und irgendwann haben wir alle Implementierungsdetails von Webbrowsern standardisiert.
Lux
4
@Joshua ältere Versionen von GNU sed (keine anderen Implementierungen!) Zeigten ebenfalls diesen Fehler, der irgendwo zwischen den Versionen 2.05 und 3.01 (vor mehr als 20 Jahren) behoben wurde. Ich vermute, es ist dort, wo dieses Verhalten seinen Ursprung hat, bevor es in Perl (wo es zu einem Feature wurde) und von dort in Javascript gelangt.
Mosvy
1
@recursive - Fair genug. Ich finde sie beide für eine Sekunde überraschend, realisiere dann "Null-Breiten-Übereinstimmung" und bin nicht länger überrascht. :-)
TJ Crowder

Antworten:

98

Gemäß dem ECMA-262- Standard ruft String.prototype.replace RegExp.prototype [@@ replace] auf , das besagt:

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).

wo rxist /.*/gund Sist 'asdf'.

Siehe 11.c.iii.2.b:

b. Sei nextIndex AdvanceStringIndex (S, thisIndex, fullUnicode).

Deshalb 'asdf'.replace(/.*/g, 'x')ist darin eigentlich:

  1. Ergebnis (undefiniert), Ergebnisse = [], lastIndex =0
  2. Ergebnis = 'asdf', Ergebnisse = [ 'asdf' ], lastIndex =4
  3. Ergebnis = ''ergibt = [ 'asdf', '' ], liest = 4, AdvanceStringIndexsetzen liest zu5
  4. Ergebnis = null, Ergebnisse = [ 'asdf', '' ], Rückgabe

Daher gibt es 2 Übereinstimmungen.

Alan Liang
quelle
42
Diese Antwort erfordert, dass ich sie studiere, um sie zu verstehen.
Felipe
Die TL; DR ist, dass sie mit einer 'asdf'leeren Zeichenfolge übereinstimmt ''.
Jimh
34

Gemeinsam in einem Offline-Chat mit yawkat haben wir eine intuitive Methode gefunden , um zu verstehen, warum"abcd".replace(/.*/g, "x") genau zwei Übereinstimmungen erzeugt werden. Beachten Sie, dass wir nicht überprüft haben, ob es vollständig der vom ECMAScript-Standard auferlegten Semantik entspricht. Nehmen Sie dies daher als Faustregel.

Faustregeln

  • Betrachten Sie die Übereinstimmungen als eine Liste von Tupeln (matchStr, matchIndex)in chronologischer Reihenfolge, die angeben, welche Zeichenfolgenteile und Indizes der Eingabezeichenfolge bereits aufgefressen wurden.
  • Diese Liste wird fortlaufend beginnend links von der Eingabezeichenfolge für den regulären Ausdruck erstellt.
  • Bereits aufgefressene Teile können nicht mehr abgeglichen werden
  • Das Ersetzen erfolgt an Indizes, die durch matchIndexÜberschreiben des Teilstrings matchStran dieser Position angegeben werden. Wenn matchStr = "", dann ist der "Ersatz" effektiv Einfügen.

Formal wird der Vorgang des Abgleichs und Ersetzens als Schleife beschrieben, wie in der anderen Antwort zu sehen ist .

Einfache Beispiele

  1. "abcd".replace(/.*/g, "x")Ausgänge "xx":

    • Die Matchliste ist [("abcd", 0), ("", 4)]

      Insbesondere enthält es nicht die folgenden Übereinstimmungen, an die man aus folgenden Gründen hätte denken können:

      • ("a", 0), ("ab", 0): Der Quantifizierer *ist gierig
      • ("b", 1), ("bc", 1): Aufgrund des vorherigen Spiels ("abcd", 0)sind die Saiten "b"und "bc"bereits aufgefressen
      • ("", 4), ("", 4) (dh zweimal): Die Indexposition 4 ist bereits beim ersten offensichtlichen Match aufgebraucht
    • Daher "x"ersetzt die Ersetzungszeichenfolge die gefundenen Übereinstimmungszeichenfolgen genau an diesen Positionen: An Position 0 ersetzt sie die Zeichenfolge "abcd"und an Position 4 ersetzt sie "".

      Hier können Sie sehen, dass das Ersetzen als echtes Ersetzen eines vorherigen Strings oder nur als Einfügen eines neuen Strings dienen kann.

  2. "abcd".replace(/.*?/g, "x")mit einem Lazy Quantifier*? Ausgänge"xaxbxcxdx"

    • Die Matchliste ist [("", 0), ("", 1), ("", 2), ("", 3), ("", 4)]

      Im Gegensatz zum vorherigen Beispiel, hier ("a", 0), ("ab", 0), ("abc", 0), oder auch ("abcd", 0)nicht wegen der Trägheit des quantifier enthält , die es streng die kürzeste mögliche Übereinstimmung zu finden begrenzt.

    • Da alle Übereinstimmungszeichenfolgen leer sind, erfolgt keine tatsächliche Ersetzung, sondern das Einfügen xan den Positionen 0, 1, 2, 3 und 4.

  3. "abcd".replace(/.+?/g, "x") mit einer Lazy Quantifier+? Ausgänge"xxxx"

    • Die Matchliste ist [("a", 0), ("b", 1), ("c", 2), ("d", 3)]
  4. "abcd".replace(/.{2,}?/g, "x") mit einer Lazy Quantifier[2,}? Ausgänge"xx"

    • Die Matchliste ist [("ab", 0), ("cd", 2)]
  5. "abcd".replace(/.{0}/g, "x")Ausgänge "xaxbxcxdx"nach der gleichen Logik wie in Beispiel 2.

Härtere Beispiele

Wir können die Idee des Einfügens anstelle des Ersetzens konsequent ausnutzen, wenn wir nur immer eine leere Zeichenfolge abgleichen und die Position steuern, an der solche Übereinstimmungen zu unserem Vorteil stattfinden. Zum Beispiel können wir reguläre Ausdrücke erstellen, die mit der leeren Zeichenfolge an jeder geraden Position übereinstimmen, um dort ein Zeichen einzufügen:

  1. "abcdefgh".replace(/(?<=^(..)*)/g, "_"))mit einem positiven Lookbehind(?<=...) Ausgängen "_ab_cd_ef_gh_"(nur in Chrome so weit unterstützt)

    • Die Matchliste ist [("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]
  2. "abcdefgh".replace(/(?=(..)*$)/g, "_"))mit einem positiven Lookahead-(?=...) Ausgang"_ab_cd_ef_gh_"

    • Die Matchliste ist [("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]
ComFreek
quelle
4
Ich denke, es ist ein bisschen schwierig, es intuitiv zu nennen (und zwar fett). Für mich sieht es eher nach Stockholm-Syndrom und post-hoc-Rationalisierung aus. Ihre Antwort ist gut, übrigens, ich beschwere mich nur über JS-Design oder mangelndes Design.
Eric Duminil
7
@EricDuminil Das habe ich mir zuerst auch gedacht, aber nachdem ich die Antwort geschrieben habe, scheint der skizzierte Global-Regex-Replace-Algorithmus genau so zu sein, wie man es sich vorstellen würde, wenn man von vorne anfangen würde. Es ist wie while (!input not eaten up) { matchAndEat(); }. Die obigen Kommentare weisen auch darauf hin, dass das Verhalten vor langer Zeit vor der Existenz von JavaScript entstanden ist.
ComFreek
2
Der Teil, der immer noch keinen Sinn ergibt (aus einem anderen Grund als „das sagt der Standard“), ist, dass die Übereinstimmung mit vier Zeichen ("abcd", 0)nicht die Position 4 einnimmt, an die das folgende Zeichen gehen würde, die Übereinstimmung mit null Zeichen ("", 4)jedoch iss die Position 4, wo das folgende Zeichen hingehen würde. Wenn ich dies von Grund auf neu entwerfen würde, würde ich die Regel verwenden, (str2, ix2)die (str1, ix1)iff folgen könnte ix2 >= ix1 + str1.length() && ix2 + str2.length() > ix1 + str1.length(), was diese Fehlfunktion nicht verursacht.
Anders Kaseorg
2
@AndersKaseorg ("abcd", 0)isst Position 4 nicht, weil sie "abcd"nur 4 Zeichen lang ist und daher nur die Indizes 0, 1, 2, 3 isst. Ich kann sehen, woher Ihre Argumentation kommen könnte: Warum können wir keine ("abcd" ⋅ ε, 0)5 Zeichen lange Übereinstimmung haben, bei der ⋅ stimmt εdie Verkettung und die Breite Null überein? Formal weil "abcd" ⋅ ε = "abcd". Ich habe in den letzten Minuten über einen intuitiven Grund nachgedacht, aber keinen gefunden. Ich denke, man muss immer so behandeln, εals würde man nur so alleine auftreten wie "". Ich würde gerne mit einer alternativen Implementierung ohne diesen Fehler oder diese Leistung spielen.
ComFreek
1
Wenn die vierstellige Zeichenfolge vier Indizes enthalten soll, sollte die nullstellige Zeichenfolge keine Indizes enthalten. Alle Überlegungen, die Sie zu einem anstellen, sollten auch für den anderen gelten (z. B. "" ⋅ ε = ""obwohl ich nicht sicher bin, welchen Unterschied Sie zwischen ""und machen möchten ε, was dasselbe bedeutet). Der Unterschied kann also nicht als intuitiv erklärt werden - er ist es einfach.
Anders Kaseorg
26

Das erste Match ist offensichtlich "asdf"(Position [0,4]). Da das globale Flag ( g) gesetzt ist, wird die Suche fortgesetzt. An diesem Punkt (Position 4) findet es eine zweite Übereinstimmung, eine leere Zeichenfolge (Position [4,4]).

Denken Sie daran, dass *null oder mehr Elemente übereinstimmen.

David SK
quelle
4
Warum also nicht drei Spiele? Am Ende könnte es ein weiteres leeres Match geben. Es gibt genau zwei. Diese Erklärung erklärt, warum es zwei geben könnte , aber nicht, warum es statt eins oder drei geben sollte.
rekursiv
7
Nein, es gibt keine andere leere Zeichenfolge. Weil diese leere Zeichenfolge gefunden wurde. eine leere Zeichenfolge an der Position 4,4. Sie wird als eindeutiges Ergebnis erkannt. Eine Übereinstimmung mit der Bezeichnung "4,4" kann nicht wiederholt werden. Wahrscheinlich können Sie denken, dass sich an der Position [0,0] eine leere Zeichenfolge befindet, aber der Operator * gibt das maximal mögliche Element zurück. Dies ist der Grund, warum nur 4,4 möglich sind
David SK
16
Wir müssen uns daran erinnern, dass reguläre Ausdrücke keine regulären Ausdrücke sind. In regulären Ausdrücken gibt es unendlich viele leere Zeichenfolgen zwischen jeweils zwei Zeichen sowie am Anfang und am Ende. In Regexes gibt es genau so viele leere Zeichenfolgen, wie es die Spezifikation für den speziellen Geschmack der Regex-Engine angibt.
Jörg W Mittag
7
Dies ist nur eine post-hoc-Rationalisierung.
Mosvy
9
@mosvy außer dass es die genaue Logik ist, die tatsächlich verwendet wird.
Hobbs
1

Der erste xist einfach für den Ersatz des Matchings asdf.

Sekunde xfür die leere Zeichenfolge nach asdf. Die Suche wird beendet, wenn sie leer ist.

Nilanka Manoj
quelle