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

132

Saya menemukan fakta mengejutkan (bagi saya).

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

Mengapa dua penggantian? Tampaknya string yang tidak kosong tanpa baris baru akan menghasilkan dua penggantian tepat untuk pola ini. Menggunakan fungsi penggantian, saya bisa melihat bahwa penggantian pertama adalah untuk seluruh string, dan yang kedua adalah untuk string kosong.

rekursif
sumber
9
contoh yang lebih sederhana: "asdf".match(/.*/g)return ["asdf", ""]
Narro
32
Karena bendera global (g). Bendera global memungkinkan untuk pencarian lain untuk memulai di akhir pertandingan sebelumnya, sehingga menemukan string kosong.
Celsiuss
6
dan mari kita jujur: mungkin tidak ada yang menginginkan perilaku itu. itu mungkin detail implementasi dari keinginan "aa".replace(/b*/, "b")untuk menghasilkan babab. Dan pada titik tertentu kami menstandarkan semua detail implementasi webbrowser.
Lux
4
@Joshua versi lama GNU sed (bukan implementasi lain!) Juga menunjukkan bug ini, yang diperbaiki di suatu tempat antara rilis 2.05 dan 3.01 (20+ tahun yang lalu). Saya menduga itu ada di mana perilaku ini berasal, sebelum membuat jalan ke perl (di mana ia menjadi fitur) dan dari sana ke javascript.
Mosvy
1
@recursive - Cukup adil. Saya menemukan mereka berdua terkejut sejenak, kemudian menyadari "pertandingan nol-lebar" dan tidak lagi terkejut. :-)
TJ Crowder

Jawaban:

98

Sesuai standar ECMA-262 , String.prototype.replace panggilan RegExp.prototype [@@ replace] , yang mengatakan:

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

dimana rx adalah /.*/gdan Sadalah 'asdf'.

Lihat 11.c.iii.2.b:

b. Biarkan nextIndex menjadi AdvanceStringIndex (S, thisIndex, fullUnicode).

Oleh karena itu dalam 'asdf'.replace(/.*/g, 'x') dalamnya sebenarnya:

  1. hasil (tidak ditentukan), hasil = [] , lastIndex =0
  2. hasil = 'asdf', hasil =[ 'asdf' ] , lastIndex =4
  3. hasil = '', hasil = [ 'asdf', '' ], lastIndex = 4,AdvanceStringIndex , mengatur lastIndex ke5
  4. hasil = null, hasil =[ 'asdf', '' ] , kembali

Karena itu ada 2 pertandingan.

Alan Liang
sumber
42
Jawaban ini mengharuskan saya untuk mempelajarinya agar dapat memahaminya.
Felipe
TL; DR cocok 'asdf'dan mengosongkan string ''.
jimh
34

Bersama dalam obrolan offline dengan yawkat , kami menemukan cara yang intuitif melihat mengapa "abcd".replace(/.*/g, "x")persis menghasilkan dua kecocokan. Perhatikan bahwa kami belum memeriksa apakah itu benar-benar sama dengan semantik yang dipaksakan oleh standar ECMAScript, maka anggap saja sebagai aturan praktis.

Aturan Jempol

  • Pertimbangkan korek api sebagai daftar tupel (matchStr, matchIndex) dalam urutan kronologis yang menunjukkan bagian string mana dan indeks dari string input yang telah dimakan.
  • Daftar ini terus dibangun mulai dari kiri string input untuk regex.
  • Bagian yang sudah dimakan tidak dapat ditandingi lagi
  • Penggantian dilakukan pada indeks yang diberikan dengan matchIndexmenimpa substring matchStrpada posisi itu. Jika matchStr = "", maka "penggantian" secara efektif dimasukkan.

Secara formal, tindakan pencocokan dan penggantian digambarkan sebagai lingkaran seperti yang terlihat pada jawaban lainnya .

Contoh mudah

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

    • Daftar pertandingan adalah [("abcd", 0), ("", 4)]

      Khususnya, itu tidak termasuk pertandingan berikut yang orang bisa pikirkan karena alasan berikut:

      • ("a", 0), ("ab", 0): quantifier *serakah
      • ("b", 1), ("bc", 1): karena pertandingan sebelumnya ("abcd", 0), senar "b"dan "bc"sudah dimakan
      • ("", 4), ("", 4) (Yaitu dua kali): posisi indeks 4 sudah dimakan oleh pertandingan nyata pertama
    • Oleh karena itu, string pengganti "x"menggantikan string yang ditemukan tepat pada posisi tersebut: pada posisi 0 itu menggantikan string "abcd"dan pada posisi 4 itu menggantikan"" .

      Di sini Anda dapat melihat bahwa penggantian dapat berfungsi sebagai penggantian yang benar dari string sebelumnya atau hanya sebagai penyisipan string baru.

  2. "abcd".replace(/.*?/g, "x")dengan keluaran kuantifier malas*?"xaxbxcxdx"

    • Daftar pertandingan adalah [("", 0), ("", 1), ("", 2), ("", 3), ("", 4)]

      Berbeda dengan contoh sebelumnya, di sini ("a", 0), ("ab", 0), ("abc", 0), atau bahkan ("abcd", 0)tidak termasuk karena kemalasan quantifier yang ketat membatasi untuk menemukan pertandingan yang sesingkat mungkin.

    • Karena semua string kecocokan kosong, tidak ada penggantian yang sebenarnya terjadi, melainkan penempatan xpada posisi 0, 1, 2, 3, dan 4.

  3. "abcd".replace(/.+?/g, "x")dengan keluaran kuantifier malas+?"xxxx"

    • Daftar pertandingan adalah [("a", 0), ("b", 1), ("c", 2), ("d", 3)]
  4. "abcd".replace(/.{2,}?/g, "x")dengan keluaran kuantifier malas[2,}?"xx"

    • Daftar pertandingan adalah [("ab", 0), ("cd", 2)]
  5. "abcd".replace(/.{0}/g, "x")output "xaxbxcxdx"dengan logika yang sama seperti pada contoh 2.

Contoh yang lebih sulit

Kami dapat secara konsisten mengeksploitasi gagasan penyisipan dan bukan penggantian jika kami hanya selalu mencocokkan string kosong dan mengontrol posisi di mana kecocokan tersebut terjadi untuk keuntungan kami. Sebagai contoh, kita dapat membuat ekspresi reguler yang cocok dengan string kosong di setiap posisi genap untuk menyisipkan karakter di sana:

  1. "abcdefgh".replace(/(?<=^(..)*)/g, "_"))dengan lookbehind positif(?<=...) output "_ab_cd_ef_gh_"(hanya didukung di Chrome sejauh ini)

    • Daftar pertandingan adalah [("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]
  2. "abcdefgh".replace(/(?=(..)*$)/g, "_"))dengan output lookahead positif(?=...)"_ab_cd_ef_gh_"

    • Daftar pertandingan adalah [("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]
ComFreek
sumber
4
Saya pikir ini agak sulit untuk menyebutnya intuitif (dan dicetak tebal). Bagi saya itu lebih mirip sindrom Stockholm dan rasionalisasi pasca-hoc. Jawaban Anda baik, BTW, saya hanya mengeluh tentang desain JS, atau kurangnya desain dalam hal ini.
Eric Duminil
7
@EricDuminil Saya juga berpikir begitu pada awalnya, tetapi setelah menulis jawabannya, algoritma global-regex-replace yang dibuat sketsa tampaknya persis seperti yang akan muncul jika seseorang mulai dari awal. Itu seperti while (!input not eaten up) { matchAndEat(); }. Juga, komentar di atas menunjukkan bahwa perilaku berasal dari jauh sebelum keberadaan JavaScript.
ComFreek
2
Bagian yang masih tidak masuk akal (untuk alasan lain selain "itulah yang dikatakan standar") adalah bahwa pertandingan empat karakter ("abcd", 0)tidak memakan posisi 4 di mana karakter berikut akan pergi, namun pertandingan karakter nol ("", 4)tidak makan posisi 4 di mana karakter berikut akan pergi. Jika saya mendesain ini dari awal, saya pikir aturan yang akan saya gunakan adalah yang (str2, ix2)mungkin mengikuti (str1, ix1)iff ix2 >= ix1 + str1.length() && ix2 + str2.length() > ix1 + str1.length(), yang tidak menyebabkan kesalahan ini.
Anders Kaseorg
2
@AndersKaseorg ("abcd", 0)tidak memakan posisi 4 "abcd"karena panjangnya hanya 4 karakter dan karenanya hanya makan indeks 0, 1, 2, 3. Saya bisa melihat dari mana alasan Anda berasal: mengapa kita tidak bisa memiliki ("abcd" ⋅ ε, 0)pertandingan 5 karakter di mana ⋅ Apakah gabungan dan εkesesuaian nol-lebar? Karena secara formal "abcd" ⋅ ε = "abcd". Saya memikirkan alasan intuitif untuk menit-menit terakhir, tetapi gagal menemukannya. Saya kira kita harus selalu memperlakukan εhanya sebagai terjadi dengan sendirinya "". Saya ingin bermain dengan implementasi alternatif tanpa bug atau fitur itu, silakan bagikan!
ComFreek
1
Jika string empat karakter harus makan empat indeks, maka string karakter nol tidak boleh makan indeks. Alasan apa pun yang Anda buat tentang satu harus sama-sama berlaku untuk yang lain (misalnya "" ⋅ ε = "", meskipun saya tidak yakin perbedaan apa yang ingin Anda tarik antara ""dan ε, yang berarti hal yang sama). Jadi perbedaannya tidak bisa dijelaskan sebagai intuitif — memang begitu.
Anders Kaseorg
26

Pertandingan pertama jelas "asdf"(Posisi [0,4]). Karena flag global ( g) diatur, ia melanjutkan pencarian. Pada titik ini (Posisi 4), ia menemukan kecocokan kedua, string kosong (Posisi [4,4]).

Ingat bahwa *cocok dengan nol atau lebih elemen.

David SK
sumber
4
Jadi mengapa tidak tiga pertandingan? Mungkin ada pertandingan kosong lain di akhir. Tepatnya ada dua. Penjelasan ini menjelaskan mengapa mungkin ada dua, tetapi tidak mengapa harus ada satu atau tiga.
Rekursif
7
Tidak, tidak ada string kosong lainnya. Karena string kosong itu telah ditemukan. string kosong pada posisi 4,4, Terdeteksi sebagai hasil yang unik. Kecocokan dengan label "4,4" tidak dapat diulang. mungkin Anda dapat berpikir bahwa ada string kosong di posisi [0,0] tetapi operator * mengembalikan elemen sebanyak mungkin. inilah alasan mengapa hanya 4,4 yang dimungkinkan
David SK
16
Kita harus ingat bahwa regex bukan ekspresi reguler. Dalam ekspresi reguler, ada banyak string kosong di antara setiap dua karakter, serta di awal dan di akhir. Dalam regex, ada banyak string kosong seperti spesifikasi untuk rasa mesin regex tertentu.
Jörg W Mittag
7
Ini hanya rasionalisasi pasca-hoc.
Mosvy
9
@ Mosvy kecuali bahwa itu adalah logika yang sebenarnya digunakan.
hobbs
1

sederhananya, yang pertama xadalah untuk penggantian yang cocok asdf.

kedua xuntuk string kosong sesudahnya asdf. Pencarian berakhir ketika kosong.

Nilanka Manoj
sumber