なぜ「asdf」.replace(/.*/ g、「x」)==「xx」なのですか?


131

私は(私にとって)驚くべき事実に出会いました。

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

なぜ2つの代替品があるのですか?改行のない空でない文字列は、このパターンのちょうど2つの置換を生成するようです。置換関数を使用すると、最初の置換は文字列全体に対するものであり、2番目の置換は空の文字列に対するものであることがわかります。


9
もっと簡単な例:"asdf".match(/.*/g)リターン[ "ASDF"、 ""]
Narro

32
グローバル(g)フラグのため。グローバルフラグを使用すると、前の一致の最後から別の検索を開始できるため、空の文字列が見つかります。
摂氏

6
正直に言うと、おそらくそのような動作を望んでいる人はいないでしょう。おそらく、を実現したい実装の詳細でし"aa".replace(/b*/, "b")babab。そして、ある時点で、ウェブブラウザのすべての実装の詳細を標準化しました。
ラックス

4
@Joshuaの古いバージョンのGNU sed(他の実装ではありません!)にもこのバグがあり、2.05と3.01リリースの間(20年以上前)のどこかで修正されました。私はそれがperl(それが機能になったところ)に入る前、そしてそこからjavascriptに入る前に、この振る舞いが始まった場所にあると思います。
モスビー

1
@recursive-十分に公平です。どちらも一瞬意外であることに気づき、「ゼロ幅一致」を実現しました。:-)
TJクロウダー

回答:


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/.*/gSあります'asdf'

11.c.iii.2.bを参照してください。

b。nextIndexをAdvanceStringIndex(S、thisIndex、fullUnicode)とします。

したがって、'asdf'.replace(/.*/g, 'x')実際には:

  1. 結果(未定義)、結果= []、lastIndex =0
  2. 結果= 'asdf'、結果= [ 'asdf' ]、lastIndex =4
  3. 結果= ''、結果= [ 'asdf', '' ]、lastIndexの= 4AdvanceStringIndex、セットlastIndexのへ5
  4. 結果= null、結果= [ 'asdf', '' ]、戻り値

したがって、2つの一致があります。


42
この答えはそれを理解するために私がそれを研究することを必要とします。
フェリペ

TL; DRは'asdf'空の文字列と一致するということ''です。
jimh

34

yawkatとのオフラインチャットで、2つの一致が正確に生成される理由を直感的に理解できる方法を見つけました"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) (つまり2回):インデックス位置4は、最初の見かけ上の一致によってすでに使い尽くされています
    • したがって、置換文字列"x"は、見つかった一致文字列をこれらの位置で正確"abcd"に置き換え""ます。位置0で文字列を置き換え、位置4で置き換えます。

      ここで、置換が前の文字列の真の置換として、または新しい文字列の挿入として機能することがわかります。

  2. "abcd".replace(/.*?/g, "x")怠惰な数量詞の*?出力"xaxbxcxdx"

    • マッチリストは [("", 0), ("", 1), ("", 2), ("", 3), ("", 4)]

      前の例とは対照的に、ここでは("a", 0)("ab", 0)("abc", 0)、あるいは("abcd", 0)厳密に可能な限り短いマッチを見つけるために、それを制限する数量詞の怠慢によるものは含まれません。

    • すべての一致文字列が空であるため、実際の置換は行われず、代わりにx0、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, "_"))正の後読みの(?<=...)出力"_ab_cd_ef_gh_"(のみこれまでChromeでサポートされています)

    • マッチリストは [("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]
  2. "abcdefgh".replace(/(?=(..)*$)/g, "_"))肯定先読みの(?=...)出力"_ab_cd_ef_gh_"

    • マッチリストは [("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]

4
私はそれを直感的に(そして太字で)呼ぶとは少し伸びると思います。私には、それはストックホルム症候群とその場しのぎの合理化に似ています。正解です。ところで、私はJSのデザイン、またはその点についてのデザインの欠如についてのみ不満を述べています。
Eric Duminil

7
@EricDuminil最初は私もそう考えましたが、答えを書いた後、スケッチされたglobal-regex-replaceアルゴリズムは、ゼロから始めた場合とまったく同じように考えられます。みたいwhile (!input not eaten up) { matchAndEat(); }です。また、上記のコメントは、この動作がJavaScriptが存在するずっと前に発生したことを示しています。
ComFreek

2
まだ意味をなさない部分(「それが標準が言っていること」以外の理由で)は、4文字の一致("abcd", 0)は、次の文字が移動する位置4を食べないが、0文字の一致("", 4)はするということです次のキャラクターが行くポジション4を食べる。私がこれを一から設計している場合、私が使用するルール(str2, ix2)(str1, ix1)iff 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文字の長い試合として⋅連結とεゼロ幅一致は何ですか?正式に"abcd" ⋅ ε = "abcd"。最後の数分は直感的な理由を考えましたが、見つかりませんでした。私は常にε、それ自体で発生するのと同じように扱う必要があると思い""ます。そのバグや特技なしで代替実装を試してみたい、気軽に共有してください!
ComFreek

1
4文字の文字列が4つのインデックスを食べる場合、0文字の文字列はインデックスを食べません。(例えば、あなたが1程度になるかもしれない任意の推論は、同様に他に適用されるべきである"" ⋅ ε = ""私はあなたが間を描画するつもりは何の区別はないよにもかかわらず、""およびε同じことを意味しています)。そのため、違いは直感的であると説明することはできません。
Anders Kaseorg

26

最初の一致は明らかに"asdf"(位置[0,4])です。グローバルフラグ(g)が設定されているため、検索が続行されます。この時点(位置4)では、2番目の一致である空の文字列(位置[4,4])が見つかります。

*ゼロ個以上の要素に一致することを覚えておいてください。


4
では、なぜ3試合ではないのですか?最後に別の空の一致がある可能性があります。正確には2つあります。この説明は、なぜ2つある可能性があるのかを説明していますが、1つまたは3つではなくなぜある必要があるのかを説明していません。
再帰的

7
いいえ、他の空の文字列はありません。空の文字列が見つかったからです。位置4、4の空の文字列は、一意の結果として検出されます。「4,4」というラベルの付いた試合は繰り返すことができません。おそらく、[0,0]の位置に空の文字列があると考えることができますが、*演算子は要素の最大可能数を返します。これが4,4のみが可能である理由です
David SK

16
正規表現は正規表現ではないことを覚えておく必要があります。正規表現では、2文字ごとの間、および最初と最後に無限に多くの空の文字列があります。正規表現では、正規表現エンジンの特定のフレーバーの仕様にあると同じ数の空の文字列があります。
イェルクWミッターク

7
これは単なる事後的な合理化です。
モスビー

9
@mosvyは、実際に使用されているロジックそのものです。
ホブ

1

簡単に言うと、1つ目xはマッチングの置換ですasdf

x後の空の文字列の2番目asdf。空になると検索は終了します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.