“asdf”.replace (/.*/ g,“x”) ==“xx”인 이유는 무엇입니까?


129

나는 (나에게) 놀라운 사실을 발견했다.

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

두 번 교체해야합니까? 줄 바꿈이없는 비어 있지 않은 문자열은이 패턴을 정확히 두 번 대체하는 것으로 보입니다. 교체 기능을 사용하면 첫 번째 교체가 전체 문자열에 대한 것이고 두 번째 교체는 빈 문자열에 대한 것임을 알 수 있습니다.


9
더 간단한 예 : "asdf".match(/.*/g)return [ "asdf", ""]
Narro

32
글로벌 (g) 플래그 때문입니다. 전역 플래그를 사용하면 이전 검색이 끝날 때 다른 검색을 시작하여 빈 문자열을 찾을 수 있습니다.
섭씨

6
솔직하게 말하면 아마도 아무도 그 행동을 정확히 원하지 않았을 것입니다. 아마도 "aa".replace(/b*/, "b")결과 를 얻고 자하는 구현 세부 사항 일 것 입니다 babab. 그리고 어느 시점에서 우리는 웹 브라우저의 모든 구현 세부 사항을 표준화했습니다.
럭스

4
@Joshua 이전 버전의 GNU sed (다른 구현은 아님!)도이 버그를 나타 냈으며 ,이 버그는 2.05와 3.01 릴리스 (20 년 전) 사이에서 수정되었습니다. 나는 그것이 펄 (기능이 된 곳)과 자바 스크립트로 들어가기 전에이 행동이 시작된 곳이라고 생각합니다.
mosvy

1
@ 재귀-충분합니다. 나는 둘 다 놀랍게도 발견하고 "제로 너비 일치"를 실현하고 더 이상 놀라지 않습니다. :-)
TJ Crowder

답변:


98

당으로 ECMA-262 표준, String.prototype.replace는 호출 RegExp.prototype [대체 @@] 말한다 :

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. 결과 (정의되지 않음), 결과 = [], lastIndex =0
  2. 결과 = 'asdf', 결과 = [ 'asdf' ], lastIndex =4
  3. result = '', results = [ 'asdf', '' ], lastIndex = 4,, AdvanceStringIndexlastIndex를 다음으로 설정하십시오.5
  4. 결과 = null, 결과 = [ 'asdf', '' ], 반환

따라서 2 개의 일치 항목이 있습니다.


42
이 답변을 이해하려면 그것을 연구해야합니다.
Felipe

TL; DR은 일치 'asdf'하고 빈 문자열 ''입니다.
jimh

34

yawkat 과의 오프라인 채팅에서 두 개의 일치 항목을 정확히 생성 하는 직관적 인 방법 을 찾았 "abcd".replace(/.*/g, "x")습니다. 우리는 그것이 ECMAScript 표준에 의해 부과 된 의미론과 완전히 동일한 지 여부를 확인하지 않았으므로, 단지 경험의 법칙으로 간주합니다.

엄지 손가락의 규칙

  • (matchStr, matchIndex)입력 문자열의 문자열 부분과 색인이 이미 섭취 된 것을 나타내는 연대순 으로 튜플 목록으로 일치를 고려하십시오 .
  • 이 목록은 정규식에 대한 입력 문자열의 왼쪽부터 지속적으로 작성됩니다.
  • 이미 먹은 부분은 더 이상 일치시킬 수 없습니다
  • 해당 위치에서 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")A의 지연 정량화 된*? 출력"xaxbxcxdx"

    • 경기 목록은 [("", 0), ("", 1), ("", 2), ("", 3), ("", 4)]

      앞의 예는 대조적으로, 여기에 ("a", 0), ("ab", 0), ("abc", 0), 또는 ("abcd", 0)때문에 엄밀하게는 최단 일치하는 항목을 찾습니다 제한하는 정량의 게으름에 포함되지 않습니다.

    • 모든 일치 문자열이 비어 있으므로 실제 대체는 발생하지 않지만 대신 x0, 1, 2, 3 및 4 위치에 삽입 됩니다.

  3. "abcd".replace(/.+?/g, "x")A의 지연 정량화 된+? 출력"xxxx"

    • 경기 목록은 [("a", 0), ("b", 1), ("c", 2), ("d", 3)]
  4. "abcd".replace(/.{2,}?/g, "x")A의 지연 정량화 된[2,}? 출력"xx"

    • 경기 목록은 [("ab", 0), ("cd", 2)]
  5. "abcd".replace(/.{0}/g, "x")"xaxbxcxdx"예제 2와 동일한 로직으로 출력 합니다.

더 어려운 예

항상 빈 문자열을 일치시키고 이러한 일치가 발생하는 위치를 제어하는 ​​경우 교체 대신 삽입 아이디어를 일관되게 활용할 수 있습니다 . 예를 들어, 모든 짝수 위치에서 빈 문자열과 일치하는 정규식을 만들어 거기에 문자를 삽입 할 수 있습니다.

  1. "abcdefgh".replace(/(?<=^(..)*)/g, "_"))A의 포지티브 lookbehind에(?<=...) 출력한다 "_ab_cd_ef_gh_"(단, 지금까지 크롬 지원)

    • 경기 목록은 [("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]
  2. "abcdefgh".replace(/(?=(..)*$)/g, "_"))A의 긍정적 예측(?=...) 출력"_ab_cd_ef_gh_"

    • 경기 목록은 [("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]

4
나는 그것이 직관적이라고 (그리고 대담하게) 부르는 것은 약간의 스트레칭이라고 생각합니다. 나에게 그것은 스톡홀름 증후군과 사후 합리화처럼 보입니다. 당신의 대답은 좋습니다, BTW, 나는 JS 디자인이나 그 문제에 대한 디자인 부족에 대해서만 불평합니다.
Eric Duminil

7
@EricDuminil 나는 처음에 그렇게 생각했지만 대답을 작성한 후에 스케치 된 전역 정규식 대체 알고리즘은 처음부터 시작했을 때와 정확히 일치하는 것처럼 보입니다. 같습니다 while (!input not eaten up) { matchAndEat(); }. 또한 위의 의견 은 행동이 JavaScript가 존재하기 오래 전에 시작된 것임을 나타냅니다.
ComFreek

2
여전히 이해가되지 않는 부분 ( "표준이 말하는 것"이외의 다른 이유로)은 4 자 일치 ("abcd", 0)는 다음 문자가가는 위치 4를 먹지 않지만 0 자 일치 ("", 4)는하지 않는다는 것입니다. 다음 캐릭터가 갈 위치 4를 먹는다. 내가 처음부터 이것을 설계했다면, 내가 사용하는 규칙은 iff를 (str2, ix2)따르는 규칙이라고 생각합니다 . (str1, ix1)ix2 >= ix1 + str1.length() && ix2 + str2.length() > ix1 + str1.length()
Anders Kaseorg

2
@AndersKaseorg ("abcd", 0)위치 4 becaues을 먹지 않고는 "abcd"당신의 추론에서 올 수있는 곳은 4 자 길이 따라서 만 먹는다 지수 0, 1, 2, 3. 내가 볼 수 있습니다 : 왜 우리가 할 수없는 ("abcd" ⋅ ε, 0)경우 5 자 길이의 일치로 ⋅ 연결과 ε너비가 0입니까? 공식적으로 "abcd" ⋅ ε = "abcd". 나는 마지막 순간에 대한 직관적 인 이유에 대해 생각했지만 찾지 못했습니다. 나는 항상 ε스스로 발생 하는 것처럼 취급해야한다고 생각합니다 "". 그 버그 나 위업없이 대체 구현을하고 싶습니다. 공유하십시오!
ComFreek

1
4 개의 문자열이 4 개의 인덱스를 먹어야한다면, 0 개의 문자열은 인덱스를 먹지 않아야합니다. 하나에 대해 할 수있는 모든 추론은 다른 하나에도 동일하게 적용되어야합니다 (예 : 와 , "" ⋅ ε = ""사이에 어떤 차이점을 그릴 것인지 확실하지 않지만 ). 따라서 차이점을 직관적으로 설명 할 수는 없습니다. ""ε
Anders Kaseorg

26

첫 번째 일치는 분명히 "asdf"(위치 [0,4])입니다. 글로벌 플래그 ( g)가 설정 되었으므로 계속 검색합니다. 이 시점에서 (Position 4) 두 번째 일치하는 빈 문자열 (Position [4,4])을 찾습니다.

기억 *일치 이상의 요소를 제로.


4
그렇다면 왜 세 경기가 아닌가? 끝에 다른 빈 일치가있을 수 있습니다. 정확히 두 가지가 있습니다. 이 설명은 왜 두 가지 가있을 있는지 설명 하지만 왜 하나 나 세 가지가 있어야하는지 는 설명 하지 않습니다.
재귀

7
아니요, 다른 빈 문자열은 없습니다. 그 빈 문자열이 발견 되었기 때문에. 위치 4,4의 빈 문자열, 고유 한 결과로 감지됩니다. "4,4"로 표시된 일치는 반복 할 수 없습니다. [0,0] 위치에 빈 문자열이 있다고 생각할 수 있지만 * 연산자는 가능한 최대 요소를 반환합니다. 이것이 단지 4,4 만 가능한 이유입니다
David SK

16
정규 표현식은 정규 표현식이 아니라는 것을 기억해야합니다. 정규 표현식에서 두 문자 사이와 시작과 끝 사이에는 빈 문자열이 무한히 많이 있습니다. 정규 표현식에는 정규 표현식 엔진의 특정 특징에 대한 사양만큼 빈 문자열이 있습니다.
Jörg W Mittag

7
이것은 단지 사후 합리화입니다.
mosvy

9
실제로 사용되는 정확한 논리라는 점을 제외하고 @mosvy.
홉스
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.