Dlaczego „asdf” .replace (/.*/ g, „x”) == „xx”?


129

Natknąłem się na zaskakujący (dla mnie) fakt.

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

Dlaczego dwie zastępstwa? Wygląda na to, że jakikolwiek niepusty ciąg bez znaków nowej linii da dokładnie dwa zamienniki dla tego wzorca. Za pomocą funkcji zamiany widzę, że pierwsza zamiana dotyczy całego łańcucha, a druga pustego łańcucha.


9
prostszy przykład: "asdf".match(/.*/g)return [„asdf”, „”]
Narro

32
Z powodu globalnej flagi (g). Flaga globalna pozwala na rozpoczęcie kolejnego wyszukiwania na końcu poprzedniego dopasowania, znajdując pusty ciąg.
Celsiuss

6
i bądźmy szczerzy: prawdopodobnie nikt nie chciał dokładnie takiego zachowania. był to prawdopodobnie szczegół implementacji, którego "aa".replace(/b*/, "b")skutkiem było babab. W pewnym momencie ustandaryzowaliśmy wszystkie szczegóły implementacji przeglądarek internetowych.
Lux

4
@Joshua starsze wersje GNU sed (nie inne implementacje!) Również wykazywały ten błąd, który został naprawiony gdzieś pomiędzy wersjami 2.05 i 3.01 (ponad 20 lat temu). Podejrzewam, że tam właśnie powstało takie zachowanie, zanim trafił do perla (gdzie stał się cechą), a stamtąd do javascript.
mosvy

1
@recursive - Wystarczająco uczciwy. Oboje zaskakują mnie na sekundę, a potem zdaję sobie sprawę z „dopasowania zerowej szerokości” i nie jestem już zaskoczony. :-)
TJ Crowder

Odpowiedzi:


98

Zgodnie ze standardem ECMA-262 String.prototype.replace wywołuje RegExp.prototype [@@ replace] , który mówi:

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

gdzie rxjest /.*/gi Sjest 'asdf'.

Patrz 11.c.iii.2.b:

b. Niech nextIndex będzie AdvanceStringIndex (S, thisIndex, fullUnicode).

Dlatego w 'asdf'.replace(/.*/g, 'x')rzeczywistości jest to:

  1. wynik (niezdefiniowany), results = [], lastIndex =0
  2. wynik = 'asdf', wyniki = [ 'asdf' ], lastIndex =4
  3. Wynik = ''wyniki = [ 'asdf', '' ], = lastIndex 4, AdvanceStringIndexustaw lastIndex do5
  4. wynik = null, wyniki = [ 'asdf', '' ], powrót

Dlatego są 2 mecze.


42
Ta odpowiedź wymaga ode mnie przestudiowania, aby ją zrozumieć.
Felipe

TL; DR oznacza, że ​​pasuje 'asdf'i pusty ciąg ''.
jimh

34

Wspólnie na czacie offline z yawkat znaleźliśmy intuicyjny sposób sprawdzenia, dlaczego "abcd".replace(/.*/g, "x")dokładnie produkuje dwa dopasowania. Zauważ, że nie sprawdziliśmy, czy całkowicie równa się semantyce narzuconej przez standard ECMAScript, dlatego po prostu weź to za ogólną zasadę.

Reguły kciuka

  • Rozważ dopasowania jako listę krotek (matchStr, matchIndex)w kolejności chronologicznej, które wskazują, które części łańcucha i wskaźniki łańcucha wejściowego zostały już zjedzone.
  • Ta lista jest stale budowana, zaczynając od lewej strony łańcucha wejściowego wyrażenia regularnego.
  • Części już zjedzone nie mogą być już dopasowane
  • Zastąpienie odbywa się według wskaźników podanych przez matchIndexzastąpienie podłańcucha matchStrw tej pozycji. Jeśli matchStr = "", to „zamiana” polega na wstawieniu.

Formalnie czynność dopasowywania i zastępowania jest opisana jako pętla, jak widać w drugiej odpowiedzi .

Proste przykłady

  1. "abcd".replace(/.*/g, "x")wyjścia "xx":

    • Lista meczów to [("abcd", 0), ("", 4)]

      W szczególności nie obejmuje następujących dopasowań, o których można pomyśleć z następujących powodów:

      • ("a", 0), ("ab", 0): kwantyfikator *jest zachłanny
      • ("b", 1), ("bc", 1): ze względu na poprzedni mecz ("abcd", 0)struny "b"i "bc"są już zjedzone
      • ("", 4), ("", 4) (tj. dwa razy): pozycja indeksu 4 jest już pochłonięta przez pierwsze pozorne dopasowanie
    • Dlatego ciąg "x"zastępujący zastępuje znalezione pasujące ciągi dokładnie w tych pozycjach: w pozycji 0 zastępuje ciąg, "abcd"a w pozycji 4 zastępuje "".

      Tutaj widać, że zamiana może działać jak prawdziwe zastąpienie poprzedniego ciągu lub po prostu wstawienie nowego ciągu.

  2. "abcd".replace(/.*?/g, "x")z leniwymi*? wyjściami kwantyfikatora"xaxbxcxdx"

    • Lista meczów to [("", 0), ("", 1), ("", 2), ("", 3), ("", 4)]

      W przeciwieństwie do poprzedniego przykładu, tutaj ("a", 0), ("ab", 0), ("abc", 0), lub nawet ("abcd", 0)nie są wliczone powodu lenistwa kwantyfikatora, że ściśle ogranicza to, aby znaleźć możliwie najkrótszy mecz.

    • Ponieważ wszystkie pasujące ciągi znaków są puste, nie występuje faktyczna zamiana, ale zamiast tego wstawienia xpozycji 0, 1, 2, 3 i 4.

  3. "abcd".replace(/.+?/g, "x")z leniwymi+? wyjściami kwantyfikatora"xxxx"

    • Lista meczów to [("a", 0), ("b", 1), ("c", 2), ("d", 3)]
  4. "abcd".replace(/.{2,}?/g, "x")z leniwymi[2,}? wyjściami kwantyfikatora"xx"

    • Lista meczów to [("ab", 0), ("cd", 2)]
  5. "abcd".replace(/.{0}/g, "x")wyprowadza "xaxbxcxdx"tą samą logiką jak w przykładzie 2.

Trudniejsze przykłady

Możemy konsekwentnie wykorzystywać ideę wstawiania zamiast zamiany, jeśli zawsze dopasowujemy pusty ciąg i kontrolujemy pozycję, w której takie dopasowania przynoszą nam korzyść. Na przykład możemy utworzyć wyrażenia regularne pasujące do pustego łańcucha w każdej parzystej pozycji, aby wstawić tam znak:

  1. "abcdefgh".replace(/(?<=^(..)*)/g, "_"))z pozytywnym wyglądem za(?<=...) wyjściem "_ab_cd_ef_gh_"(do tej pory obsługiwane tylko w Chrome)

    • Lista meczów to [("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]
  2. "abcdefgh".replace(/(?=(..)*$)/g, "_"))z A pozytywne lookAhead(?=...) wyjść"_ab_cd_ef_gh_"

    • Lista meczów to [("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]

4
Myślę, że to trochę trudne nazwać to intuicyjnie (i to odważnie). Dla mnie to bardziej przypomina syndrom sztokholmski i racjonalizację post-hoc. Twoja odpowiedź jest dobra, BTW, narzekam tylko na projekt JS lub na brak projektu w tej sprawie.
Eric Duminil

7
@EricDuminil Na początku też tak myślałem, ale po napisaniu odpowiedzi naszkicowany globalny algorytm zastępowania wyrażeń regularnych wydaje się być dokładnie taki, jak wymyśliłby go, gdyby zaczął od zera. To jest jak while (!input not eaten up) { matchAndEat(); }. Ponadto powyższe komentarze wskazują, że zachowanie powstało dawno temu przed istnieniem JavaScript.
ComFreek

2
Część, która nadal nie ma sensu (z innego powodu niż „to co średnia mówi”) jest to, że mecz cztery charakter ("abcd", 0)nie jeść pozycji 4, gdzie następujących znaków pójdzie, jeszcze mecz zero postać ("", 4)robi zjedz pozycję 4, do której poszedłaby następująca postać. Gdybym projektował to od zera, myślę, że zastosowałbym zasadę, która (str2, ix2)może być zgodna z (str1, ix1)iff ix2 >= ix1 + str1.length() && ix2 + str2.length() > ix1 + str1.length(), co nie powoduje tej mylności.
Anders Kaseorg,

2
@AndersKaseorg ("abcd", 0)nie je pozycji 4, ponieważ "abcd"ma tylko 4 znaki długości, a więc po prostu je indeksy 0, 1, 2, 3. Widzę, skąd pochodzi twoje rozumowanie: dlaczego nie możemy mieć ("abcd" ⋅ ε, 0)5-znakowego dopasowania, gdzie ⋅ jest konkatenacja i εdopasowanie zerowej szerokości? Formalnie ponieważ "abcd" ⋅ ε = "abcd". Myślałem o intuicyjnym celu ostatnich minut, ale nie udało mi się go znaleźć. Wydaje mi się, że zawsze należy traktować to εtak, jakby występowało samo z siebie "". Chciałbym zagrać z alternatywną implementacją bez tego błędu lub wyczynu. Podziel się!
ComFreek

1
Jeśli czteroznakowy ciąg powinien jeść cztery indeksy, to ciąg zerowego znaku nie powinien jeść żadnych indeksów. Wszelkie argumenty, które możesz poczynić na temat jednego, powinny w równym stopniu odnosić się do drugiego (np. "" ⋅ ε = ""Chociaż nie jestem pewien, jakie rozróżnienie zamierzasz rozróżnić między ""i ε, co oznacza to samo). Tak więc różnicy nie można wyjaśnić jako intuicyjnej - po prostu jest.
Anders Kaseorg,

26

Pierwszy mecz to oczywiście "asdf"(pozycja [0,4]). Ponieważ ustawiono flagę globalną ( g), kontynuuje wyszukiwanie. W tym momencie (pozycja 4) znajduje drugie dopasowanie, pusty ciąg znaków (pozycja [4,4]).

Pamiętaj, że *pasuje do zera lub więcej elementów.


4
Dlaczego więc nie trzy mecze? Na końcu może być inny pusty mecz. Są dokładnie dwa. To wyjaśnienie wyjaśnia, dlaczego mogą istnieć dwa, ale nie dlaczego powinno być zamiast jednego lub trzech.
rekurencyjny

7
Nie, nie ma innego pustego ciągu. Ponieważ znaleziono ten pusty ciąg. pusty ciąg na pozycji 4,4, jest wykrywany jako unikalny wynik. Meczu oznaczonego „4,4” nie można powtórzyć. prawdopodobnie możesz pomyśleć, że w pozycji [0,0] jest pusty łańcuch, ale operator * zwraca maksymalną możliwą liczbę elementów. z tego powodu możliwe jest tylko 4,4
David SK

16
Musimy pamiętać, że wyrażenia regularne nie są wyrażeniami regularnymi. W wyrażeniach regularnych istnieje nieskończenie wiele pustych ciągów znaków między dwoma znakami, a także na początku i na końcu. W wyrażeniach regularnych jest dokładnie tyle pustych ciągów znaków, ile podano w specyfikacji konkretnego smaku silnika wyrażeń regularnych.
Jörg W Mittag

7
To tylko racjonalizacja post-hoc.
mosvy

9
@mosvy oprócz tego, że jest to dokładnie taka logika, która jest faktycznie używana.
hobbs
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.