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

rec*_*ive 153 javascript regex

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

console.log("asdf".replace(/.*/g, "x"));
Run Code Online (Sandbox Code Playgroud)

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

小智 111

根据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).
Run Code Online (Sandbox Code Playgroud)

这里rx/.*/gS'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, AdvanceStringIndex, 将 lastIndex 设置为5
  4. 结果 = null, 结果 = [ 'asdf', '' ], 返回

因此有 2 场比赛。

  • 这个答案需要我去研究它才能理解它。 (47认同)

Com*_*eek 39

在与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")带有惰性量词*?输出"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, "_"))具有正向后视(?<=...)输出"_ab_cd_ef_gh_"(目前仅在 Chrome 中受支持)

    • 比赛名单是 [("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]
  2. "abcdefgh".replace(/(?=(..)*$)/g, "_"))具有正向预测(?=...)输出"_ab_cd_ef_gh_"

    • 比赛名单是 [("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]

  • @EricDuminil 一开始我也是这么认为的,但是在写完答案之后,草绘的全局正则表达式替换算法似乎正是从头开始时提出的方法。就像 `while (!input 没有吃完) { matchAndEat(); }`。另外,[上面的评论](/sf/ask/4288420601/?noredirect=1#comment108417048_61263151)表明该行为源于很久以前JavaScript存在之前。 (8认同)
  • 我认为称其为直观(并且以粗体表示)有点牵强。对我来说,这更像是斯德哥尔摩综合症和事后合理化。你的回答很好,顺便说一句,我只是抱怨 JS 设计,或者缺乏这方面的设计。 (4认同)
  • 仍然没有意义的部分(出于“这就是标准所说的”之外的任何其他原因)是四字符匹配 `("abcd", 0)` 不会吃掉后面字符将出现的位置 4 go,但零字符匹配 `("", 4)` 确实会占用下一个字符所在的位置 4。如果我从头开始设计,我认为我使用的规则是 `(str2, ix2)` 可以遵循 `(str1, ix1)` iff `ix2 &gt;= ix1 + str1.length() &amp;&amp; ix2 + str2 .length() &gt; ix1 + str1.length()`,这不会导致此错误功能。 (2认同)
  • @AndersKaseorg `("abcd", 0)` 不吃位置 4,因为 `"abcd"` 只有 4 个字符长,因此只吃索引 0, 1, 2, 3。我可以看到你的推理可能来自哪里:为什么我们不能将 `("abcd" ⋅ ε, 0)` 作为 5 个字符长的匹配,其中 ⋅ 是串联,而 `ε` 是零宽度匹配?形式上是因为 `"abcd" ⋅ ε = "abcd"`。我在最后几分钟思考了一个直观的原因,但未能找到。**我想人们必须始终将`ε`视为单独出现的`""`。**我很乐意尝试一种没有该错误或壮举的替代实现。请随意分享! (2认同)

Dav*_* SK 31

第一场比赛显然是"asdf"(Position [0,4])。由于设置了全局标志 ( g),它会继续搜索。此时(位置 4),它找到了第二个匹配项,一个空字符串(位置 [4,4])。

请记住,*匹配零个或多个元素。

  • 我们必须记住,正则表达式不是正则表达式。在正则表达式中,每两个字符之间以及开头和结尾处都有无限多个空字符串。在正则表达式中,空字符串的数量与正则表达式引擎特定风格的规范所述的空字符串数量完全相同。 (16认同)
  • @mosvy,除了它是实际使用的确切逻辑。 (9认同)
  • 不,没有其他空字符串。因为已经找到了那个空字符串。位置 4,4 上有一个空字符串,它被检测为唯一结果。标记为“4,4”的匹配不能重复。也许你会认为 [0,0] 位置有一个空字符串,但 * 运算符返回最大可能的元素。这就是只有 4,4 可能的原因 (7认同)
  • 这只是事后合理化。 (7认同)
  • 那么为什么不三场比赛呢?最后可能还会出现另一场空场比赛。正好有两个。这个解释解释了为什么“可能”有两个,但没有解释为什么应该有而不是一到三个。 (4认同)