为什么“asdf”.replace(/.*/g, “x”) == “xx”?

2022-08-30 02:42:52

我偶然发现了一个令人惊讶的(对我来说)事实。

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

为什么有两个替代品?似乎任何没有换行符的非空字符串都将为此模式产生两个替换。使用替换函数,我可以看到第一个替换是针对整个字符串,第二个是针对空字符串。


答案 1

根据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/.*/gS'asdf'

见11.c.iii.2.b:

b.让 nextIndex be AdvanceStringIndex(S, thisIndex, fullUnicode)。

因此,它实际上是:'asdf'.replace(/.*/g, 'x')

  1. result (undefined), results = , lastIndex =[]0
  2. 结果 = , 结果 = , 最后索引 ='asdf'[ 'asdf' ]4
  3. result = , results = , lastIndex = , , 将 lastIndex 设置为''[ 'asdf', '' ]4AdvanceStringIndex5
  4. 结果 = , 结果 = , 返回null[ 'asdf', '' ]

因此有2场比赛。


答案 2

在与yawkat的离线聊天中,我们一起找到了一种直观的方式来了解为什么会产生两场比赛。请注意,我们尚未检查它是否完全等于 ECMAScript 标准强加的语义,因此仅将其作为经验法则。"abcd".replace(/.*/g, "x")

经验法则

  • 将匹配项视为按时间顺序排列的元组列表,这些元组指示输入字符串的哪些字符串部分和索引已被占用。(matchStr, matchIndex)
  • 此列表从正则表达式的输入字符串左侧开始不断构建。
  • 已经吃掉的零件无法再匹配
  • 替换是在给定的索引处通过覆盖该位置的子字符串来完成的。如果 ,则“替换”实际上是插入。matchIndexmatchStrmatchStr = ""

从形式上讲,匹配和替换的行为被描述为一个循环,如其他答案所示

简单示例

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

    • 匹配列表为[("abcd", 0), ("", 4)]

      值得注意的是,由于以下原因,它不包括人们可能想到的以下匹配项:

      • ("a", 0), : 量词是贪婪的("ab", 0)*
      • ("b", 1), : 由于之前的比赛,琴弦和已经吃掉了("bc", 1)("abcd", 0)"b""bc"
      • ("", 4), ("", 4)(即两次):索引位置 4 已经被第一个明显的匹配所吞噬
    • 因此,替换字符串将恰好在这些位置替换找到的匹配字符串:在位置 0 处,它将替换字符串,在位置 4 处,它将替换 。"x""abcd"""

      在这里,您可以看到替换可以作为上一个字符串的真正替换,也可以作为新字符串的插入。

  2. "abcd".replace(/.*?/g, "x")使用惰性量词 *? 输出"xaxbxcxdx"

    • 匹配列表为[("", 0), ("", 1), ("", 2), ("", 3), ("", 4)]

      与前面的示例相反,此处 、、、 甚至不包括在内,因为量词的懒惰严格限制了它找到尽可能短的匹配项。("a", 0)("ab", 0)("abc", 0)("abcd", 0)

    • 由于所有匹配字符串都是空的,因此不会发生实际的替换,而是在位置 0、1、2、3 和 4 处插入。x

  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")按与示例 2 中相同的逻辑输出。"xaxbxcxdx"

更难的例子

我们可以始终如一地利用插入而不是替换的想法,如果我们总是匹配一个空字符串并控制这种匹配对我们有利的位置。例如,我们可以在每个偶数位置创建与空字符串匹配的正则表达式,以在其中插入一个字符:

  1. "abcdefgh".replace(/(?<=^(..)*)/g, "_"))具有正面外观(?<=...)输出(到目前为止仅在Chrome中受支持)"_ab_cd_ef_gh_"

    • 匹配列表为[("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]
  2. "abcdefgh".replace(/(?=(..)*$)/g, "_"))具有正面的展望 (?=...) 输出"_ab_cd_ef_gh_"

    • 匹配列表为[("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]