ทำไม“ asdf” .replace (/.*/ g,“ x”) ==“ xx”


131

ฉันเจอความจริงที่น่าประหลาดใจ (กับฉัน)

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

ทำไมต้องเปลี่ยนทั้งสอง ดูเหมือนว่าสตริงที่ไม่ว่างโดยไม่มีบรรทัดใหม่จะสร้างการแทนที่สองรูปแบบสำหรับรูปแบบนี้ ใช้ฟังก์ชั่นการแทนที่ฉันจะเห็นว่าการแทนที่ครั้งแรกสำหรับทั้งสายและที่สองคือสำหรับสตริงที่ว่างเปล่า


9
ตัวอย่างที่ง่ายขึ้น: "asdf".match(/.*/g)ส่งคืน ["asdf", ""]
Narro

32
เนื่องจากการตั้งค่าสถานะ (g) การตั้งค่าสถานะส่วนกลางช่วยให้การค้นหาอื่นเริ่มต้นเมื่อสิ้นสุดการแข่งขันก่อนหน้าดังนั้นจึงค้นหาสตริงว่าง
Celsiuss

6
และให้ความซื่อสัตย์: คงไม่มีใครอยากได้พฤติกรรมนั้นอย่างแน่นอน มันอาจจะเป็นรายละเอียดการดำเนินการที่ต้องการจะส่งผลให้"aa".replace(/b*/, "b") bababและในบางจุดเราได้สร้างมาตรฐานรายละเอียดการใช้งานทั้งหมดของเว็บเบราว์เซอร์
Lux

4
@Joshua เวอร์ชันเก่าของGNU sed (ไม่ใช่การใช้งานอื่น ๆ !) ก็แสดงข้อผิดพลาดนี้เช่นกันซึ่งได้รับการแก้ไขระหว่าง 2.05 ถึง 3.01 รุ่น (20+ ปีที่แล้ว) ฉันสงสัยว่ามันอยู่ที่นั่นพฤติกรรมนี้เกิดขึ้นก่อนที่จะเข้าสู่ Perl (ซึ่งมันกลายเป็นคุณสมบัติ) และจากที่นั่นลงในจาวาสคริปต์
mosvy

1
@recursive - ยุติธรรมเพียงพอ ฉันพบว่าทั้งคู่น่าแปลกใจเป็นวินาทีจากนั้นตระหนักถึง "การจับคู่แบบความกว้างเป็นศูนย์" และฉันไม่แปลกใจอีกต่อไป :-)
TJ Crowder

คำตอบ:


98

ตามมาตรฐานECMA-262 String.prototype.replace จะเรียกRegExp.prototype [@@ replace]ซึ่งบอกว่า:

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

ที่rxเป็น/.*/gและเป็นS'asdf'

ดู 11.c.iii.2.b:

ข ให้ nextIndex เป็น AdvanceStringIndex (S, thisIndex, fullUnicode)

ดังนั้นในความ'asdf'.replace(/.*/g, 'x')เป็นจริง:

  1. ผลลัพธ์ (ไม่ได้กำหนด), results = [], lastIndex =0
  2. ผลลัพธ์ = 'asdf', ผลลัพธ์ = [ 'asdf' ], lastIndex =4
  3. ผล = ''ผล = [ 'asdf', '' ], lastIndex = 4, AdvanceStringIndexตั้ง lastIndex ไป5
  4. result = null, results = [ 'asdf', '' ], return

ดังนั้นจึงมี 2 แมทช์


42
คำตอบนี้ต้องการให้ฉันศึกษาเพื่อที่จะเข้าใจมัน
เฟลิเป้

สินค้า TL; DR ก็คือว่ามันตรงและสตริงที่ว่างเปล่า'asdf' ''
jimh

34

ร่วมกันในการแชทแบบออฟไลน์กับyawkatเราพบวิธีที่เข้าใจง่ายว่าทำไม"abcd".replace(/.*/g, "x")สร้างการแข่งขันสองรายการ โปรดทราบว่าเรายังไม่ได้ตรวจสอบว่ามันเท่ากับความหมายที่กำหนดโดยมาตรฐาน ECMAScript อย่างสมบูรณ์หรือไม่ดังนั้นจึงถือว่าเป็นกฎง่ายๆ

กฎของ Thumb

  • พิจารณาการจับคู่เป็นรายการของ tuples (matchStr, matchIndex)ตามลำดับเวลาที่ระบุว่าส่วนสตริงและดัชนีของสายป้อนได้ถูกกินแล้ว
  • รายการนี้สร้างขึ้นอย่างต่อเนื่องโดยเริ่มจากด้านซ้ายของสตริงป้อนสำหรับ regex
  • ส่วนที่กินแล้วจะไม่สามารถจับคู่ได้อีกต่อไป
  • การแทนที่จะทำที่ดัชนีที่กำหนดโดยการmatchIndexเขียนทับสตริงย่อยmatchStrที่ตำแหน่งนั้น ถ้าหากmatchStr = """การแทนที่" นั้นเป็นการแทรกที่มีประสิทธิภาพ

อย่างเป็นทางการการกระทำของการจับคู่และเปลี่ยนคำอธิบายที่เป็นห่วงเท่าที่เห็นในคำตอบอื่น

ตัวอย่างง่าย ๆ

  1. "abcd".replace(/.*/g, "x")เอาท์พุท"xx":

    • รายการแข่งขันคือ [("abcd", 0), ("", 4)]

      โดยเฉพาะอย่างยิ่งมันไม่รวมการแข่งขันต่อไปนี้อย่างใดอย่างหนึ่งอาจคิดด้วยเหตุผลต่อไปนี้:

      • ("a", 0), ("ab", 0): ตัวบอกปริมาณ*เป็นโลภ
      • ("b", 1), ("bc", 1): เนื่องจากการแข่งขันก่อนหน้านี้("abcd", 0)สาย"b"และ"bc"ถูกกินแล้ว
      • ("", 4), ("", 4) (เช่นสองครั้ง): ตำแหน่งดัชนี 4 กินแล้วโดยการจับคู่ที่ชัดเจนครั้งแรก
    • ดังนั้นสตริงทดแทน"x"แทนที่สตริงการแข่งขันพบว่าที่ตำแหน่งเหล่านั้นที่ตำแหน่ง 0 แทนที่สตริง"abcd"และที่ตำแหน่ง 4 ""แทนที่

      ที่นี่คุณจะเห็นว่าการแทนที่สามารถทำหน้าที่แทนจริงของสตริงก่อนหน้าหรือเพียงแค่การแทรกของสตริงใหม่

  2. "abcd".replace(/.*?/g, "x")ด้วยเอาท์พุตที่ขี้เกียจ*?"xaxbxcxdx"

    • รายการแข่งขันคือ [("", 0), ("", 1), ("", 2), ("", 3), ("", 4)]

      ในทางตรงกันข้ามกับตัวอย่างก่อนหน้านี้ที่นี่("a", 0), ("ab", 0), ("abc", 0)หรือแม้กระทั่ง("abcd", 0)จะไม่รวมอยู่เนื่องจากความเกียจคร้านของปริมาณที่ จำกัด อย่างเคร่งครัดเพื่อหาการจับคู่ที่สั้นที่สุด

    • เนื่องจากสตริงการจับคู่ทั้งหมดว่างเปล่าจะไม่มีการแทนที่เกิดขึ้นจริง แต่จะแทนที่การแทรกxที่ตำแหน่ง 0, 1, 2, 3 และ 4

  3. "abcd".replace(/.+?/g, "x")ด้วยเอาท์พุตที่ขี้เกียจ+?"xxxx"

    • รายการแข่งขันคือ [("a", 0), ("b", 1), ("c", 2), ("d", 3)]
  4. "abcd".replace(/.{2,}?/g, "x")ด้วยเอาท์พุตที่ขี้เกียจ[2,}?"xx"

    • รายการแข่งขันคือ [("ab", 0), ("cd", 2)]
  5. "abcd".replace(/.{0}/g, "x")เอาต์พุต"xaxbxcxdx"โดยลอจิกเดียวกับในตัวอย่างที่ 2

ตัวอย่างที่ยากขึ้น

เราสามารถใช้ความคิดอย่างสม่ำเสมอในการแทรกแทนการแทนที่ถ้าเราเพียงแค่จับคู่สตริงว่าง ๆ และควบคุมตำแหน่งที่การจับคู่ดังกล่าวเกิดขึ้นเพื่อประโยชน์ของเรา ตัวอย่างเช่นเราสามารถสร้างนิพจน์ทั่วไปที่ตรงกับสตริงว่างที่ทุกตำแหน่งคู่เพื่อแทรกอักขระที่นั่น:

  1. "abcdefgh".replace(/(?<=^(..)*)/g, "_"))กับlookbehind บวก(?<=...)เอาท์พุท"_ab_cd_ef_gh_"(การสนับสนุนเฉพาะใน Chrome เพื่อให้ห่างไกล)

    • รายการแข่งขันคือ [("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]
  2. "abcdefgh".replace(/(?=(..)*$)/g, "_"))ด้วยเอาต์พุตlookahead เชิงบวก(?=...)"_ab_cd_ef_gh_"

    • รายการแข่งขันคือ [("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]

4
ฉันคิดว่ามันเป็นอะไรที่ค่อนข้างยืดยาวที่จะเรียกมันว่าเป็นสัญชาตญาณ สำหรับฉันมันดูเหมือนกับอาการของสตอกโฮล์มและการหาเหตุผลเข้าข้างตนเองหลังการเปลี่ยนแปลง คำตอบของคุณดี BTW ฉันแค่บ่นเกี่ยวกับการออกแบบ JS หรือขาดการออกแบบสำหรับเรื่องนั้น
Eric Duminil

7
@EricDuminil ฉันคิดอย่างนั้นเหมือนกันในตอนแรก แต่หลังจากเขียนคำตอบแล้วอัลกอริทึม global-regex-replace ที่ร่างดูเหมือนจะเป็นวิธีที่จะเกิดขึ้นกับมันถ้าใครเริ่มจากศูนย์ while (!input not eaten up) { matchAndEat(); }มันก็เหมือนกับ นอกจากนี้ความคิดเห็นด้านบนระบุว่าพฤติกรรมมีต้นกำเนิดมานานแล้วก่อนการมีอยู่ของ JavaScript
ComFreek

2
ส่วนที่ยังไม่ได้ทำให้รู้สึก (ด้วยเหตุผลอื่น ๆ ที่ไม่ใช่“นั่นคือสิ่งที่มาตรฐานกล่าวว่า”) คือการจับคู่สี่ตัวอักษร("abcd", 0)ไม่กินตำแหน่ง 4 ที่ตัวละครต่อไปนี้จะไปยังการแข่งขันศูนย์ตัวอักษร("", 4)ไม่ กินตำแหน่งที่ 4 ซึ่งตัวละครต่อไปนี้จะไป หากฉันกำลังออกแบบสิ่งนี้ตั้งแต่เริ่มต้นฉันคิดว่ากฎที่ฉันใช้คือ(str2, ix2)อาจเป็นไปตาม(str1, ix1)iff ix2 >= ix1 + str1.length() && ix2 + str2.length() > ix1 + str1.length()ซึ่งไม่ทำให้เกิดความผิดพลาดนี้
Anders Kaseorg

2
@AndersKaseorg ("abcd", 0)ไม่กินตำแหน่งที่ 4 "abcd"เพราะมีความยาวเพียง 4 ตัวอักษรและเพียงแค่กินดัชนี 0, 1, 2, 3 ฉันสามารถดูได้ว่าเหตุผลของคุณมาจากที่ใด: ทำไมเราไม่สามารถ("abcd" ⋅ ε, 0)จับคู่แบบยาว 5 ตัวที่⋅ การต่อข้อมูลและεการจับคู่ความกว้างเป็นศูนย์คืออะไร "abcd" ⋅ ε = "abcd"อย่างเป็นทางการเพราะ ฉันคิดถึงเหตุผลที่เข้าใจง่ายสำหรับนาทีสุดท้าย แต่ไม่สามารถหาเหตุผลได้ ฉันเดาว่าจะต้องปฏิบัติεเหมือนเกิดขึ้นกับตัวเอง""เสมอ ฉันชอบที่จะเล่นกับการใช้งานทางเลือกโดยไม่มีข้อผิดพลาดหรือความสำเร็จนั้นให้แบ่งปัน!
ComFreek

1
หากสตริงอักขระสี่ตัวควรกินดัชนีสี่ตัวดังนั้นสตริงอักขระศูนย์จะไม่กินดัชนีใด ๆ เหตุผลใดก็ตามที่คุณอาจใช้กับอีกข้อหนึ่งควรใช้กับอีกฝ่ายอย่างเท่าเทียมกัน (เช่น"" ⋅ ε = ""แม้ว่าฉันไม่แน่ใจว่าคุณตั้งใจจะแยกความแตกต่างระหว่าง""และεซึ่งหมายถึงสิ่งเดียวกัน) ดังนั้นความแตกต่างจึงไม่สามารถอธิบายได้อย่างง่าย
Anders Kaseorg

26

เห็นได้ชัดว่านัดแรก"asdf"(ตำแหน่ง [0,4]) เนื่องจากมีการตั้งค่าสถานะส่วนกลาง ( g) จึงทำการค้นหาต่อไป ณ จุดนี้ (ตำแหน่ง 4) พบการจับคู่ที่สองสตริงว่าง (ตำแหน่ง [4,4])

จำไว้ว่า*ตรงกับองค์ประกอบที่เป็นศูนย์หรือมากกว่า


4
เหตุใดจึงไม่เลือกสามนัด อาจมีการจับคู่ที่ว่างเปล่าอีกในตอนท้าย มีสองอย่างแม่นยำ คำอธิบายนี้อธิบายว่าทำไมอาจมีสอง แต่ไม่ใช่เหตุผลที่ควรมีแทนที่จะเป็นหนึ่งหรือสาม
เรียกซ้ำ

7
ไม่ไม่มีสตริงว่างอื่น ๆ เนื่องจากพบสตริงที่ว่างเปล่า สตริงว่างในตำแหน่ง 4,4 ตรวจพบว่าเป็นผลลัพธ์ที่ไม่ซ้ำกัน การแข่งขันที่มีป้ายกำกับ "4,4" ไม่สามารถทำซ้ำได้ บางทีคุณอาจคิดว่ามีสตริงว่างในตำแหน่ง [0,0] แต่ตัวดำเนินการ * จะคืนค่าองค์ประกอบที่เป็นไปได้สูงสุด นี่คือเหตุผลที่เป็นไปได้เพียง 4,4 เท่านั้น
David SK

16
เราต้องจำไว้ว่า regexes ไม่ใช่การแสดงออกปกติ ในนิพจน์ทั่วไปมีสตริงว่างจำนวนมากอยู่ระหว่างตัวละครทั้งสองทุกตัวรวมถึงตอนต้นและตอนท้าย ใน regexes มีสตริงว่างมากพอ ๆ กับข้อกำหนดสำหรับรสชาติเฉพาะของโปรแกรม regex ที่บอกว่ามี
Jörg W Mittag

7
นี่เป็นเพียงการหาเหตุผลเข้าข้างตนเองโพสต์เฉพาะกิจ
mosvy

9
@mosvy ยกเว้นว่ามันเป็นตรรกะที่แน่นอนที่ใช้จริง
ฮอบส์

1

เพียงครั้งแรกสำหรับการเปลี่ยนของการจับคู่xasdf

สองสำหรับสตริงว่างหลังx asdfการค้นหาสิ้นสุดลงเมื่อไม่มีข้อมูล

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.