Neden “asdf” .replace (/.*/ g, “x”) == “xx”?


131

Şaşırtıcı (bana göre) bir gerçekle karşılaştım.

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

Neden iki yedek? Yeni satırlar olmadan boş olmayan herhangi bir dize, bu desen için tam olarak iki yedek üretecek gibi görünüyor. Bir değiştirme işlevi kullanarak, ilk değiştirme tüm dize ve ikincisi boş bir dize olduğunu görebilirsiniz.


9
daha basit bir örnek: "asdf".match(/.*/g)return ["asdf", ""]
Narro

32
Global (g) bayrağı yüzünden. Küresel bayrak, başka bir aramanın önceki eşleşmenin sonunda başlamasına izin verir, böylece boş bir dize bulur.
Celsiuss

6
ve dürüst olalım: muhtemelen hiç kimse tam olarak bu davranışı istemiyordu. muhtemelen "aa".replace(/b*/, "b")sonuçlanmayı istemenin bir uygulama detayıydı babab. Ve bir noktada, web tarayıcılarının tüm uygulama ayrıntılarını standartlaştırdık.
Lux

4
@Joshua GNU sed'in eski sürümleri (başka uygulamalar değil!) Da 2.05 ve 3.01 sürümleri (20+ yıl önce) arasında bir yerde düzeltilen bu hatayı sergiliyordu. Perl'e (bir özellik haline geldiğinde) ve oradan javascript'e geçmeden önce bu davranışın nereden kaynaklandığından şüpheleniyorum.
mosvy

1
@recursive - Yeterince adil. İkisini de bir saniye için şaşırtıcı buluyorum, sonra "sıfır genişlikteki eşleşmeyi" fark ediyorum ve artık şaşırmıyorum. :-)
TJ Crowder

Yanıtlar:


98

Gereğince ECMA-262 standardı, String.prototype.replace çağırır RegExp.prototype [yerine @@] söyler:

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

nerede rxolduğunu /.*/gve Sbir 'asdf'.

Bkz. 11.c.iii.2.b:

b. NextIndex, AdvanceStringIndex (S, thisIndex, fullUnicode) olsun.

Bu nedenle 'asdf'.replace(/.*/g, 'x')aslında:

  1. sonuç (tanımsız), sonuç = [], lastIndex =0
  2. sonuç = 'asdf', sonuçlar = [ 'asdf' ], lastIndex =4
  3. sonuç = '', sonuçlar = [ 'asdf', '' ], lastIndex = 4,, AdvanceStringIndexlastIndex öğesini5
  4. sonuç = null, sonuçlar = [ 'asdf', '' ], döndür

Bu nedenle 2 maç var.


42
Bu cevap anlamak için çalışmamı gerektiriyor.
Felipe

TL; DR, 'asdf'boş dizeyle eşleşmesidir ''.
jimh

34

Birlikte yawkat ile çevrimdışı bir sohbette , neden tam olarak iki maç ürettiğini görmenin sezgisel bir yolunu bulduk "abcd".replace(/.*/g, "x"). ECMAScript standardının getirdiği semantiğe tamamen eşit olup olmadığını kontrol etmediğimizi, bu nedenle bunu sadece bir kural olarak kabul ettiğimizi unutmayın.

Temel Kurallar

  • Eşleşmeleri (matchStr, matchIndex), giriş dizesinin hangi dize parçalarının ve indekslerinin zaten yendiğini gösteren kronolojik sırayla tuples listesi olarak düşünün .
  • Bu liste normal ifade için giriş dizesinin solundan başlayarak sürekli olarak oluşturulur.
  • Zaten yenilmiş parçalar artık eşleştirilemez
  • Değiştirme, bu konumda matchIndexalt dize üzerine yazılarak verilen endekslerde yapılır matchStr. Eğer matchStr = "", o zaman "değiştirme" etkili bir şekilde yerleştirilir.

Resmi olarak, eşleştirme ve değiştirme eylemi , diğer cevapta görüldüğü gibi bir döngü olarak tanımlanır .

Kolay Örnekler

  1. "abcd".replace(/.*/g, "x")çıktılar "xx":

    • Maç listesi [("abcd", 0), ("", 4)]

      Özellikle, bu yok değil bir aşağıdaki nedenlerden ötürü düşünce olabilirdi aşağıdaki eşleşmeleri içerir:

      • ("a", 0), ("ab", 0): niceleyici *açgözlü
      • ("b", 1), ("bc", 1): önceki maç nedeniyle ("abcd", 0), dizeler "b"ve "bc"zaten yemiş
      • ("", 4), ("", 4) (yani iki kez): 4 numaralı indeks pozisyonu ilk görünen maç tarafından yenildi
    • Bu nedenle, yedek dize "x"bulunan eşleşme dizelerini tam olarak bu konumlarda değiştirir: 0 konumunda dizenin yerini alır "abcd"ve 4 konumunda yer değiştirir "".

      Burada, değiştirmenin önceki bir dizenin gerçek yerine veya sadece yeni bir dizenin eklenmesi olarak işlev görebildiğini görebilirsiniz.

  2. "abcd".replace(/.*?/g, "x")bir ile yavaş nicelik*? çıkışları"xaxbxcxdx"

    • Maç listesi [("", 0), ("", 1), ("", 2), ("", 3), ("", 4)]

      Önceki örnekte aksine, burada ("a", 0), ("ab", 0), ("abc", 0), hatta ("abcd", 0)nedeniyle sıkı şekilde en kısa eşleşme bulmak için sınırlar niceleyici en tembellik dahil edilmemiştir.

    • Tüm eşleşme dizeleri boş olduğundan, gerçek bir değiştirme gerçekleşmez, bunun yerine x0, 1, 2, 3 ve 4 konumlarına eklenir .

  3. "abcd".replace(/.+?/g, "x")bir ile yavaş nicelik+? çıkışları"xxxx"

    • Maç listesi [("a", 0), ("b", 1), ("c", 2), ("d", 3)]
  4. "abcd".replace(/.{2,}?/g, "x")bir ile yavaş nicelik[2,}? çıkışları"xx"

    • Maç listesi [("ab", 0), ("cd", 2)]
  5. "abcd".replace(/.{0}/g, "x")"xaxbxcxdx"örnek 2'deki ile aynı mantıkla çıktılar .

Daha Zor Örnekler

Her zaman boş bir dizeyle eşleşirsek ve bu tür eşleşmelerin avantajımızın gerçekleştiği konumu kontrol edersek , yerine değiştirme fikrinden sürekli olarak yararlanabiliriz . Örneğin, buraya bir karakter eklemek için her çift konumda boş dizeyle eşleşen normal ifadeler oluşturabiliriz:

  1. "abcdefgh".replace(/(?<=^(..)*)/g, "_"))Bir ile pozitif olan ileriye dönük(?<=...) çıkışları "_ab_cd_ef_gh_"(sadece şimdiye kadar Chrome'da desteklenir)

    • Maç listesi [("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]
  2. "abcdefgh".replace(/(?=(..)*$)/g, "_"))bir ile pozitif ileri yönlü(?=...) çıkışlar"_ab_cd_ef_gh_"

    • Maç listesi [("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]

4
Sanırım bunu sezgisel (ve cesurca) olarak adlandırmak biraz zor. Bana göre daha çok Stockholm sendromu ve post-hoc rasyonalizasyona benziyor. Cevabınız iyi, BTW, sadece JS tasarımından veya bu konuda tasarım eksikliğinden şikayet ediyorum.
Eric Duminil

7
@EricDuminil İlk başta ben de öyle düşünmüştüm, ama cevabı yazdıktan sonra, kabataslak global-regex-replace algoritması, sıfırdan başlamış olsaydı tam olarak ortaya çıkacak gibi görünüyor. Sanki while (!input not eaten up) { matchAndEat(); }. Ayrıca, yukarıdaki yorumlar , davranışın JavaScript'in varlığından çok önce ortaya çıktığını göstermektedir.
ComFreek

2
Hala ( “standart öyle diyor o” dışında başka bir nedenle) mantıklı değil o kısmı dört karakterlik maç olmasıdır ("abcd", 0)aşağıdaki karakter gider pozisyon 4 yemez, henüz sıfır karakterlik maç ("", 4)yapar Aşağıdaki karakterin gideceği 4. konumu yiyin. Sıfırdan bu tasarımı olsaydı, ben kullanımını ediyorum kural olduğunu düşünüyorum (str2, ix2)takip edebilir (str1, ix1)IFF ix2 >= ix1 + str1.length() && ix2 + str2.length() > ix1 + str1.length()bu misfeature neden yok ki.
Anders Kaseorg

2
@AndersKaseorg ("abcd", 0)pozisyon yemiyor 4 becaues "abcd"sadece 4 karakter uzunluğunda ve bu yüzden sadece 0, 1, 2, 3 indekslerini yiyor. Nedeninizin neden olabileceğini görebiliyorum: neden ("abcd" ⋅ ε, 0)5 karakterlik bir maç olarak alamıyoruz? ve εsıfır genişlik eşleşmesi nedir? Resmen çünkü "abcd" ⋅ ε = "abcd". Son dakikaların sezgisel bir nedenini düşündüm, ancak bir tane bulamadım. Sanırım biri her zaman εolduğu gibi davranmalıdır "". Ben o hata veya feat olmadan alternatif bir uygulama ile oynamak isterim., Paylaşmaktan çekinmeyin!
ComFreek

1
Dört karakter dizesi dört indeks yerse, sıfır karakter dizesi hiçbir indeks yememelidir. (Örn Eğer biri hakkında hale getirebileceğini Herhangi muhakeme eşit diğerine geçerli olmalıdır "" ⋅ ε = ""ben emin arasındaki çizmek niyetinde ne ayrım değilim rağmen ""ve εaynı anlama hangi). Yani fark sezgisel olarak açıklanamaz - basitçe öyle.
Anders Kaseorg

26

İlk maç açıktır "asdf"(Pozisyon [0,4]). Genel bayrak ( g) ayarlandığından aramaya devam eder. Bu noktada (Pozisyon 4) ikinci bir eşleşme, boş bir dize bulur (Pozisyon [4,4]).

*Sıfır veya daha fazla öğeyle eşleştiğini unutmayın .


4
Peki neden üç maç olmasın? Sonunda boş bir maç daha olabilirdi. Tam olarak iki tane var. Bu açıklama neden iki tane olabileceğini açıklıyor , ama neden bir ya da üç yerine neden olması gerektiğini açıklamıyor.
özyinelemeli

7
Hayır, başka boş dize yok. Çünkü bu boş dize bulundu. boş bir dize konumunda 4,4, benzersiz bir sonuç olarak algılanır. "4,4" etiketli bir eşleşme tekrarlanamaz. muhtemelen [0,0] konumunda boş bir dize olduğunu düşünebilirsiniz, ancak * operatörü öğelerin mümkün olan maksimum değerini döndürür. Bu sadece 4,4'ün mümkün olmasının sebebidir
David SK

16
Normal ifadelerin düzenli ifadeler olmadığını hatırlamamız gerekir. Normal ifadelerde, her iki karakter arasında, başında ve sonunda sonsuz sayıda boş dizgi vardır. Regexes'te, regex motorunun belirli lezzeti için spesifikasyonun söylediği kadar boş dizeler vardır.
Jörg W Mittag

7
Bu sadece post-hoc rasyonalizasyon.
mosvy

9
@mosvy, aslında kullanılan tam mantık dışında.
ocaklar

1

basitçe, birincisi xeşleşmenin değiştirilmesi içindir asdf.

xsonra boş dize için ikinci asdf. Arama boşken sona erer.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.