为什么“ 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。在某些时候,我们对Web浏览器的所有实现细节进行了标准化。
勒克斯

4
@Joshua较早版本的GNU sed(不是其他实现!)也表现出此错误,该错误已在205年前的2.05和3.01版本之间修复。我怀疑它是在此行为起源的地方,然后才进入perl(它成为功能)并从那里进入javascript。
mosvy

1
@recursive-足够公平。我发现他们俩都惊讶了一秒钟,然后意识到“零宽度匹配”,不再感到惊讶。:-)
TJ Crowder

Answers:


98

根据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为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进行的脱机聊天中,我们找到了一种直观的方式来了解为什么"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)]

4
我认为将其称为直观(有点粗体)有点困难。在我看来,它更像是斯德哥尔摩综合症和事后合理化。顺便说一句,您的回答是好的,顺便说一句,我只抱怨JS设计,或对此缺乏设计。
Eric Duminil

7
@EricDuminil起初我也是这么认为的,但是在写完答案后,草拟的global-regex-replace算法似乎正是从头开始的一种解决方案。就像while (!input not eaten up) { matchAndEat(); }。另外,上面的注释表明,该行为起源于JavaScript出现之前的很久以前。
ComFreek

2
仍然没有意义的部分(除了“标准所要求的”之外,还有其他原因)是四字符匹配("abcd", 0)没有占用后面字符将到达的位置4,而零字符匹配("", 4)却没有吃下一个角色将要到达的位置4。如果我是从头开始设计这个,我想我会使用的规则是(str2, ix2)可以跟随(str1, ix1)当且仅当ix2 >= ix1 + str1.length() && ix2 + str2.length() > ix1 + str1.length(),不会引起这个缺点。
Anders Kaseorg

2
@AndersKaseorg ("abcd", 0)不吃位置4,因为"abcd"长度只有4个字符,因此只吃了索引0、1、2、3。我可以看到您的推理可能来自哪里:为什么我们不能("abcd" ⋅ ε, 0)将5个字符长的匹配作为⋅和ε零宽度匹配是什么?正式因为"abcd" ⋅ ε = "abcd"。我想到了最后几分钟的直观原因,但没有找到原因。我猜一个人必须总是把它ε当成是自己发生的""我很乐意尝试一个没有该错误或壮举的替代实现。随时分享!
ComFreek

1
如果四个字符串应使用四个索引,则零字符串应不使用索引。您可能对一个做出的任何推理都应同样适用于另一个(例如"" ⋅ ε = "",尽管我不确定您打算在""和之间划出什么区别ε,这意味着同一件事)。因此,不能简单地将差异解释为直观的。
Anders Kaseorg

26

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

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


4
那为什么不参加三场比赛呢?最后可能还有另一个空的比赛。恰好有两个。这种解释解释了为什么可以有两个,但为什么不应该有一个或三个。
递归

7
不,没有其他空字符串。因为已找到该空字符串。在位置4,4上有一个空字符串,它被检测为唯一结果。标记为“ 4,4”的比赛不能重复。可能您会认为位置[0,0]处有一个空字符串,但是*运算符返回元素的最大可能值。这就是为什么只有4,4的原因
David SK

16
我们必须记住,正则表达式不是正则表达式。在正则表达式中,每两个字符之间以及在开头和结尾都有无限多个空字符串。在正则表达式中,空字符串的数量与正则表达式引擎特定风味的规范所说明的数量一样多。
约尔格W¯¯米塔格

7
这只是事后合理化。
mosvy

9
@mosvy只是实际使用的确切逻辑。
霍布斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.