为什么Javascript不支持向后断言?

26

最近我意识到(有点尴尬),在 Javascript 中无法使用正则表达式的 lookbehind assertions

为什么这个看似普遍的断言被禁用了呢?

我知道可能有其他方法来实现相同的功能,但是是基本语义禁止了这个功能,还是怎么回事呢?

同时,一些正则表达式测试工具似乎忽略了这个事实,并生成了Javascript代码——这对我来说有点奇怪。


5
你在regex101.com选择了PCRE正则表达式模式。选择JS模式,你将会收到一个错误:https://regex101.com/r/jR9uU5/4。另外,我认为回顾(look-behind)太耗费资源了,这也是它们在JS中不被支持的唯一原因。而我认为最好的回顾解决方法在这里:http://blog.stevenlevithan.com/archives/mimic-lookbehind-javascript。 - Wiktor Stribiżew
这是基于个人观点的,因为我认为只有ECMA的成员才能回答这个问题,并解释下这个决定的真正原因。:-\ - Lucas Trzesniewski
Regex101确实会警告您关于“lookbehind”的问题。您可能没有选择“javascript”。 - vks
@vks:确实,这正是stribizhev所指出的。但我有点惊讶的是,当选择为“PCRE”时,它仍然会生成Javascript代码。每次使用该网站时,我似乎都完全忽略了切换到正确的规范。它不是唯一一个似乎允许您使用错误的规范并生成(无效)代码的工具。 - l'L'l
1
@LucasTrzesniewski:我真的认为你和stribizhev提到的是非常合理的——这很有道理。我发现Waldemar在这里提到了实现的最后一件事情。从听起来的样子,似乎没有人曾经站出来帮助实现它……基本上就是为什么它从未被采纳。 - l'L'l
显示剩余2条评论
2个回答

34

今天

回溯断言现已成为ES 2018规范的官方部分。Axel Rauschmayer在他的博客文章中给出了很好的介绍

历史

看起来当时Brendan Eich还不知道它的存在(因为Netscape是建立在旧版本的Perl上):

这是1998年,我在'97年做的Netscape 4工作是基于Perl 4(!)的,但我们向ECMA TC39 TG1(JS组-当时情况不同,包括大小写)提出了一些基于Perl 5的建议。我们没有得到所有内容,而且我们不得不解决一些明显的怪癖问题。 我不记得后向引用(在Perl 5.005中于1998年7月出现)被有意地省略了。Waldemar可能会记得更多,我将JS密钥交给了他,在netscape.com内部进行mozilla.org的操作。 如果你愿意写一个提案或迷你规范(甚至是ES5风格的),请告诉我。我下周将与其他TC39成员进行讨论。 /be

邮件列表中已经有多个关于尝试包含它的讨论,但从性能角度来看,这似乎仍然是一个相当复杂的功能,因为EcmaScript正则表达式是基于回溯的,并且在使用捕获组时需要在后面进行回溯。如果使用不当,这可能会导致灾难性回溯等问题。

在某个时候,它被建议用于ES6/Es 2015,但它从未成为草案,更别提规范了。在讨论的最后一篇帖子中,似乎没有人接手实现它。如果有人感觉有兴趣编写实现,他们可以注册ES Discuss列表并提出建议。

2015年5月更新:

2015年5月,Nozomu Katō提出了ES7后行实现

2015年9月更新:

正则表达式后行被添加为阶段0建议

2017年5月更新:

该提案现在处于第3阶段。这意味着至少需要有两个浏览器来实现它才能成为下一个EcmaScript标准的一部分。正如@martixy在评论中提到的那样,Chrome已经在JS实验标志后面实现了它


2
MSDN链接与这里的后顾断言有什么关系?后顾断言如何会导致灾难性回溯? - nhahtdh
1
根据实现方式,可变长度的向后查找可能不会导致回溯。灾难性回溯通常是由模式的其他部分引起的。 - nhahtdh
1
@stribizhev:当然,你可以引用它,但它与手头的问题有什么关系吗?我找不到它说任何关于向后查找的内容。 - nhahtdh
1
@nils:lookbehind(或任何lookaround)内部的模式永远不会被回溯——这意味着一旦您退出lookaround,您不会再次回溯到其中,而是回溯到lookaround之前。但是,在验证lookaround的过程中,除了在模式内部执行的回溯之外,lookbehind本身可能会使用回溯实现或不使用回溯实现。 - nhahtdh
2
V8项目的优秀成员已经实现了这个功能。自几天前起,该功能在Chrome v49稳定版中,在实验性JavaScript标志后面可用。(http://v8project.blogspot.bg/2016/02/regexp-lookbehind-assertions.html) - martixy
显示剩余10条评论

12
从结论来看,我认为JavaScript中没有实现look-behind,因为没有人知道它应该如何表现,而且现有的实现表明添加对look-behind的支持相当复杂。
JavaScript / ECMAScript与其他语言不同之处在于规范包括正则表达式引擎的抽象实现,而大多数其他语言只停留在描述每个regex语法片段的行为上,并且对不同标记如何相互作用的描述很少。
前瞻?容易实现
前瞻的实现非常直截了当。您只需要像处理前瞻外部的模式一样处理前瞻内部的模式,并按照通常的从左到右的匹配方式进行匹配,只是在前瞻成功后,当前位置将恢复到进入前瞻之前,并且在匹配后放弃前瞻内部的选择点。
由于这是对现有自然从左到右匹配设施的非常简单的扩展,因此可以在前瞻中包含任何内容。
回顾?没那么容易
另一方面,回顾的实现并不那么直接。
想象一下您将如何实现以下回顾构造:
(?<=fixed-string)
(?<=a|fixed|string)
(?<=t[abc]{1,3})
(?<=(abc){2,6})
(?<=^.*abc.*)
(?<=\G"[^"]+");
(?<=^(.....|.......)+)
\b(\w+)\b(?<!\b\1\b.*\1)

除了基本情况 (?<=fixed-string),任何一个后顾断言实现都必须支持,(?<=a|fixed|string) 是更加理想的情况。

不同的正则表达式引擎对上述正则表达式的支持程度各不相同。

让我们看看它们在 .NET 和 Java 中是如何实现的。(这是我研究过的两种风格的后顾行为。)

.NET 实现

在 Microsoft .NET 实现中,所有这些正则表达式都是有效的,因为 .NET 使用从右到左模式实现后顾断言,并以当前位置为起始偏移量。后顾构造本身不会生成任何选择点。

然而,如果在后顾断言内使用捕获组,就会变得混乱,因为模式中的原子是从右到左解释的,如本文所示。这是这种方法的缺点:写后顾断言时需要将思维包装成从右到左。

Java 实现

相比之下,Java正则表达式实现通过重复使用从左到右的匹配功能来实现后顾引用。首先,它分析后顾引用内部的模式的最小和最大长度。然后,后顾引用是通过尝试从左到右地从“(当前位置-最小长度)”到“(当前位置-最大长度)”开始匹配内部模式来实现的。有什么遗漏吗?是的!由于我们是从左到右进行匹配的,所以我们需要确保匹配在进入后顾引用(当前位置)之前结束。在Java中,这是通过在后顾引用内部的模式末尾附加一个节点来实现的。这种实现非常低效,因为在我们谈论后顾引用内部的模式的选择点之前,就已经在后顾引用本身中创建了最大-最小+1个选择点。后顾引用边界检查也很低效,因为它位于模式的末尾,并且无法修剪那些明显超出模式中的“当前位置”的绝望选择点。

正如您所见,添加对反向前瞻的支持并不容易:

  • 从右到左的方法似乎相当有效。但是,它需要在其他现有结构上额外指定从右到左的匹配行为。
  • 重用从左到右的匹配设施的方法很复杂,并且非常低效。它还需要将模式分析引入规范中,以免性能受损。

(请注意,我尚未涵盖反向前瞻在反向后瞻内部使用以及反之的行为。在定义反向前瞻结构的语义时,这也应该考虑在内)。

这些技术障碍也被 Waldemar Horwat(编写 ES3 正则表达式规范的人)在 邮件 中提到,在 nils' answer 中引用:

没有人提交过一个关于回顾前瞻的明确定义提案。回顾前瞻很难翻译成规范使用的语言,并且在正则表达式的部分评估顺序很重要时会变得非常模糊,这就是如果涉及捕获括号时会发生的情况。你从哪里开始寻找回顾前瞻?最短的先,最长的先,还是反向字符串匹配?贪婪还是不贪婪?回溯到捕获结果中去?

1
谢谢你提供这个深入的解释。如果我理解你的回答正确的话,似乎 Javascript 处理 regex 的方式有些低效,并且他们不太可能为此实现后顾断言,对吗? - l'L'l

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接