Tại sao là As as as trực tiếp .replace (/.*/ g, hạ x x)) == xxx?


131

Tôi tình cờ thấy một sự thật đáng ngạc nhiên (với tôi).

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

Tại sao hai thay thế? Dường như bất kỳ chuỗi không trống nào không có dòng mới sẽ tạo ra chính xác hai thay thế cho mẫu này. Sử dụng một chức năng thay thế, tôi có thể thấy rằng sự thay thế đầu tiên là cho toàn bộ chuỗi và thứ hai là cho một chuỗi trống.


9
ví dụ đơn giản hơn: "asdf".match(/.*/g)return ["asdf", ""]
Narro

32
Vì cờ toàn cầu (g). Cờ toàn cầu cho phép một tìm kiếm khác bắt đầu vào cuối trận đấu trước, do đó tìm thấy một chuỗi trống.
Celsiuss

6
và hãy trung thực: có lẽ không ai muốn chính xác hành vi đó. nó có thể là một chi tiết thực hiện muốn "aa".replace(/b*/, "b")kết quả babab. Và tại một số điểm, chúng tôi đã tiêu chuẩn hóa tất cả các chi tiết triển khai của webbrowsers.
Lux

4
@Joshua các phiên bản cũ hơn của GNU sed (không phải các triển khai khác!) Cũng đã thể hiện lỗi này, đã được sửa ở đâu đó giữa các phiên bản 2.05 và 3.01 (hơn 20 năm trước). Tôi nghi ngờ đó là nơi hành vi này bắt nguồn, trước khi đi vào perl (nơi nó trở thành một tính năng) và từ đó thành javascript.
mosvy

1
@recursive - Đủ công bằng. Tôi thấy cả hai đều ngạc nhiên trong một giây, sau đó nhận ra "trận đấu có độ rộng bằng không" và không còn ngạc nhiên nữa. :-)
TJ Crowder

Câu trả lời:


98

Theo tiêu chuẩn ECMA-262 , String.prototype.replace gọi RegExp.prototype [@@ thay thế] , cho biết:

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

nơi rx/.*/gS'asdf'.

Xem 11.c.iii.2.b:

b. Đặt next Index là AdvanceString Index (S, this Index, fullUnicode).

Do đó trong 'asdf'.replace(/.*/g, 'x') đó thực sự là:

  1. kết quả (không xác định), kết quả = [] , last Index =0
  2. kết quả = 'asdf', kết quả =[ 'asdf' ] , last Index =4
  3. result = '', results = [ 'asdf', '' ], last Index = 4,AdvanceStringIndex , thiết lastIndex để5
  4. kết quả = null, kết quả =[ 'asdf', '' ] , trả lại

Do đó có 2 trận đấu.


42
Câu trả lời này đòi hỏi tôi phải nghiên cứu nó để hiểu nó.
Felipe

TL; DR là nó khớp 'asdf'và chuỗi rỗng ''.
jimh

34

Cùng nhau trong một cuộc trò chuyện ngoại tuyến với yawkat , chúng tôi đã tìm thấy một cách trực quan để xem tại sao "abcd".replace(/.*/g, "x")chính xác tạo ra hai trận đấu. Lưu ý rằng chúng tôi chưa kiểm tra xem liệu nó có hoàn toàn bằng với ngữ nghĩa được áp đặt bởi tiêu chuẩn ECMAScript hay không, do đó chỉ coi đó là quy tắc chung.

Quy tắc của ngón tay cái

  • Hãy xem các trận đấu như một danh sách các bộ dữ liệu (matchStr, matchIndex)theo thứ tự thời gian cho biết phần nào và chỉ số của chuỗi đầu vào đã được ăn hết.
  • Danh sách này liên tục được xây dựng bắt đầu từ bên trái của chuỗi đầu vào cho regex.
  • Các bộ phận đã ăn lên không thể được khớp
  • Việc thay thế được thực hiện tại các chỉ số được đưa ra bằng cách matchIndexghi đè chuỗi con matchStrở vị trí đó. Nếu matchStr = "", sau đó "thay thế" là chèn hiệu quả.

Chính thức, hành động khớp và thay thế được mô tả như một vòng lặp như đã thấy trong câu trả lời khác .

Ví dụ dễ dàng

  1. "abcd".replace(/.*/g, "x")đầu ra "xx":

    • Danh sách phù hợp là [("abcd", 0), ("", 4)]

      Đáng chú ý, nó không bao gồm các trận đấu sau đây mà người ta có thể nghĩ ra vì những lý do sau:

      • ("a", 0), ("ab", 0): bộ định lượng *là tham lam
      • ("b", 1), ("bc", 1): do trận đấu trước ("abcd", 0), các chuỗi "b""bc"đã bị ăn hết
      • ("", 4), ("", 4) (tức là hai lần): vị trí chỉ số 4 đã bị ăn mòn bởi trận đấu rõ ràng đầu tiên
    • Do đó, chuỗi "x"thay thế thay thế chính xác các chuỗi khớp được tìm thấy tại các vị trí đó: tại vị trí 0, nó thay thế chuỗi "abcd"và ở vị trí 4, nó thay thế "".

      Ở đây bạn có thể thấy rằng sự thay thế có thể đóng vai trò là sự thay thế thực sự của một chuỗi trước đó hoặc chỉ là việc chèn một chuỗi mới.

  2. "abcd".replace(/.*?/g, "x")với đầu ra định lượng lười biếng*?"xaxbxcxdx"

    • Danh sách phù hợp là [("", 0), ("", 1), ("", 2), ("", 3), ("", 4)]

      Ngược lại với ví dụ trước, ở đây ("a", 0), ("ab", 0), ("abc", 0), hoặc thậm chí ("abcd", 0)không được tính do sự lười biếng của lượng hóa mà nghiêm hạn chế nó để tìm ra trận đấu ngắn nhất có thể.

    • Vì tất cả các chuỗi khớp đều trống nên không có sự thay thế thực tế nào xảy ra mà thay vào đó là các phần chèn vào xtại các vị trí 0, 1, 2, 3 và 4.

  3. "abcd".replace(/.+?/g, "x")với đầu ra định lượng lười biếng+?"xxxx"

    • Danh sách phù hợp là [("a", 0), ("b", 1), ("c", 2), ("d", 3)]
  4. "abcd".replace(/.{2,}?/g, "x")với đầu ra định lượng lười biếng[2,}?"xx"

    • Danh sách phù hợp là [("ab", 0), ("cd", 2)]
  5. "abcd".replace(/.{0}/g, "x")đầu ra "xaxbxcxdx"theo logic tương tự như trong ví dụ 2.

Ví dụ khó hơn

Chúng ta luôn có thể khai thác ý tưởng chèn thay vì thay thế nếu chúng ta luôn luôn khớp một chuỗi trống và kiểm soát vị trí mà các trận đấu như vậy xảy ra với lợi thế của chúng ta. Ví dụ: chúng ta có thể tạo các biểu thức chính quy khớp với chuỗi trống ở mọi vị trí chẵn để chèn một ký tự ở đó:

  1. "abcdefgh".replace(/(?<=^(..)*)/g, "_"))với kết quả đầu ra tích cực(?<=...)"_ab_cd_ef_gh_" (chỉ được hỗ trợ trong Chrome)

    • Danh sách phù hợp là [("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]
  2. "abcdefgh".replace(/(?=(..)*$)/g, "_"))với đầu ra nhìn tích cực(?=...)"_ab_cd_ef_gh_"

    • Danh sách phù hợp là [("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]

4
Tôi nghĩ rằng đó là một chút căng thẳng để gọi nó là trực quan (và in đậm, ở đó). Đối với tôi nó giống như hội chứng Stockholm và hợp lý hóa hậu hoc. Câu trả lời của bạn là tốt, BTW, tôi chỉ phàn nàn về thiết kế JS, hoặc thiếu thiết kế cho vấn đề đó.
Eric Duminil

7
@EricDuminil Lúc đầu tôi cũng nghĩ vậy, nhưng sau khi viết câu trả lời, thuật toán thay thế regex toàn cầu được phác thảo dường như chính xác là cách người ta sẽ nghĩ ra nếu bắt đầu từ đầu. Nó giống như while (!input not eaten up) { matchAndEat(); }. Ngoài ra, các nhận xét ở trên chỉ ra rằng hành vi bắt nguồn từ lâu trước khi tồn tại của JavaScript.
ComFalet

2
Phần vẫn không có ý nghĩa (vì bất kỳ lý do nào khác ngoài những gì mà tiêu chuẩn nói là) là trận đấu bốn ký tự ("abcd", 0)không ăn vị trí 4 trong đó nhân vật sau sẽ đi, nhưng trận đấu ("", 4)không có ký tự nào ăn vị trí 4 nơi nhân vật sau sẽ đi. Nếu tôi đã thiết kế nó từ đầu, tôi nghĩ rằng quy tắc tôi sử dụng là (str2, ix2)có thể tuân theo (str1, ix1)iff ix2 >= ix1 + str1.length() && ix2 + str2.length() > ix1 + str1.length(), điều này không gây ra sự không thỏa đáng này.
Anders Kaseorg

2
@AndersKaseorg ("abcd", 0)không ăn vị trí 4 becau "abcd"chỉ dài 4 ký tự và do đó chỉ ăn các chỉ số 0, 1, 2, 3. Tôi có thể thấy lý do của bạn có thể đến từ đâu: tại sao chúng ta không thể có ("abcd" ⋅ ε, 0)một trận đấu dài 5 ký tự trong đó nối và εđộ rộng không khớp? Chính thức vì "abcd" ⋅ ε = "abcd". Tôi nghĩ về một lý do trực quan cho những phút cuối cùng, nhưng không tìm thấy một lý do. Tôi đoán người ta phải luôn luôn coi εnhư chỉ xảy ra một mình "". Tôi muốn chơi với một triển khai thay thế mà không có lỗi hoặc kỳ công đó., Hãy chia sẻ!
ComFalet

1
Nếu chuỗi bốn ký tự nên ăn bốn chỉ số, thì chuỗi ký tự không nên ăn không có chỉ số. Bất kỳ lý do nào bạn có thể đưa ra về một cái nên áp dụng như nhau cho cái kia (ví dụ "" ⋅ ε = "", mặc dù tôi không chắc bạn có ý định phân biệt giữa cái gì ""εcó nghĩa là điều tương tự). Vì vậy, sự khác biệt không thể được giải thích là trực quan đơn giản.
Anders Kaseorg

26

Trận đấu đầu tiên rõ ràng là "asdf"(Vị trí [0,4]). Vì cờ toàn cầu (g ) được đặt, nó tiếp tục tìm kiếm. Tại thời điểm này (Vị trí 4), nó tìm thấy một kết quả khớp thứ hai, một chuỗi trống (Vị trí [4,4]).

Hãy nhớ rằng *phù hợp với không hoặc nhiều yếu tố.


4
Vậy tại sao không phải là ba trận đấu? Có thể có một trận đấu trống ở cuối. Có chính xác hai. Giải thích này giải thích tại sao có thể có hai, nhưng không phải tại sao nên có thay vì một hoặc ba.
đệ quy

7
Không, không có chuỗi trống nào khác. Bởi vì chuỗi rỗng đã được tìm thấy. một chuỗi rỗng ở vị trí 4,4, Nó được phát hiện như một kết quả duy nhất. Một trận đấu có nhãn "4,4" không thể được lặp lại. có lẽ bạn có thể nghĩ rằng có một chuỗi rỗng ở vị trí [0,0] nhưng toán tử * trả về mức tối đa có thể của các phần tử. đây là lý do chỉ có 4,4 là có thể
David SK

16
Chúng ta phải nhớ rằng biểu thức chính quy không phải là biểu thức chính quy. Trong các biểu thức chính quy, có vô số chuỗi trống ở giữa mỗi hai ký tự, cũng như ở đầu và cuối. Trong regexes, có chính xác nhiều chuỗi trống như đặc điểm kỹ thuật cho hương vị đặc biệt của công cụ regex nói là có.
Jörg W Mittag

7
Đây chỉ là hợp lý hóa sau hoc.
mosvy

9
@mosvy ngoại trừ đó là logic chính xác thực sự được sử dụng.
hobbs

1

đơn giản, đầu tiên xlà để thay thế phù hợp asdf.

thứ hai xcho chuỗi rỗng sau asdf. Tìm kiếm chấm dứt khi trống.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.