JavaScript中动态正则表达式与内联正则表达式性能的比较

43

我偶然发现了这个性能测试,它表明JavaScript中的正则表达式不一定慢:http://jsperf.com/regexp-indexof-perf

但有一件事我没有理解:其中两种情况涉及到了我认为完全相同的东西:

RegExp('(?:^| )foo(?: |$)').test(node.className);

并且

/(?:^| )foo(?: |$)/.test(node.className);
在我看来,这两行代码完全一样,第二个是创建RegExp对象的一种简写方式。但是,它比第一个要快两倍
这些情况被称为“动态正则表达式”和“内联正则表达式”。
有人能帮我理解这两者之间的差异(以及性能差距)吗?

1
很好,“inline”版本更快,因为与使用显式构造函数相比,它要少得多的丑陋。 - Pointy
首先,您可能已经覆盖了 RegExp,因此 a) 必须查找该函数而不是直接评估它,b) 第二个可以在解析时进行评估,而第一个不能,因为调用 RegExp 可能会产生副作用,如果您已经覆盖了它。 - pimvdb
3个回答

56

现在,这里给出的答案并不完全正确/完整。

从ES5开始,字面量语法的行为与RegExp()语法相同,关于对象创建:它们都会在代码路径命中它们所参与的表达式时创建一个新的RegExp对象

因此,它们之间唯一的区别就是regexp被编译的频率

  • 使用字面量语法- 仅一次在初始代码解析和编译期间
  • 使用RegExp()语法- 每次创建新对象时

例如,请参见Stoyan Stefanov's JavaScript Patterns书:

正则表达式字面量和构造函数之间的另一个区别是,字面量只在解析时创建对象一次。如果您在循环中创建相同的正则表达式,则将返回先前创建的对象,并且已经从第一次设置了所有属性(例如lastIndex)。请考虑以下示例,以说明两次返回相同对象的情况。

function getRE() {
    var re = /[a-z]/;
    re.foo = "bar";
    return re;
}

var reg = getRE(),
    re2 = getRE();

console.log(reg === re2); // true
reg.foo = "baz";
console.log(re2.foo); // "baz"

此行为在ES5中已更改,文本也会创建新的对象。该行为已在许多浏览器环境中得到纠正,因此不能依赖它。

如果您在所有现代浏览器或NodeJS中运行此示例,则会得到以下内容:

false
bar

这意味着每次调用getRE()函数时,即使是使用文字语法的方法,也会创建一个新的RegExp对象。

上述不仅解释了为什么你不应该对不可变的正则表达式使用RegExp()(这是今天非常明显的性能问题),还解释了:

(我更惊讶的是inlineRegExp和storedRegExp有不同的结果。)

由于没有创建(和垃圾回收)新的RegExp对象的开销,所以在浏览器中,storedRegExpinlineRegExp快5%到20%。

结论:
始终使用文字语法创建不可变的正则表达式,并且如果需要重新使用,请对其进行缓存。换句话说,在低于ES5的环境中不要依赖该行为差异,并在高于该环境的环境中继续适当进行缓存。

为什么使用文字语法?与构造函数语法相比,它具有一些优点:

  1. 它更短,不会强迫你按类似类的构造函数来思考。
  2. 当使用RegExp()构造函数时,还需要转义引号并双重转义反斜杠。这使得本质上就很难读懂和理解的正则表达式更加难以理解。

(摘自同一Stoyan Stefanov的JavaScript Patterns书籍)。
因此,除非你的正则表达式在编译时未知,否则始终使用文字语法是一个好主意。


2
谢谢这个不错的更新!我也喜欢结论,尽管我会倾向于说“使用您喜欢的方式创建正则表达式、字面量或构造函数,并在需要重复使用时进行缓存”。换句话说,在ES5以下的环境中不要依赖于行为上的差异,并在ES5以上的环境中继续适当地进行缓存 :) - aaaaaa
1
@Pioul,感谢您的反馈!我已经更新了我的答案,并像您建议的那样添加了内容,除了构造函数模式的部分。请看我的回答为什么:) - Alexander Abakumov
1
@Pioul,虽然我已经从我的答案中删除了过时的部分,但我觉得这应该是预期的答案。所以,你能否接受这个答案?这将从我的答案中移除勾选标记。谢谢! - Arjan
@Arjan,谢谢你!这是你真正值得尊敬的行为! - Alexander Abakumov
1
好玩的事实是:请参见dldnh答案的最后一条评论 ;-) 开个玩笑,最佳答案应该被接受! - Arjan
1
做得好!即使六年后,也能保持更新,这点值得赞扬! - aaaaaa

12

性能差异与使用的语法有一定关系:在/pattern/RegExp(/pattern/)中,正则表达式仅被编译一次,但对于RegExp('pattern'),表达式在每次使用时都要被编译。关于此,请参见Alexander的答案,该答案今天应该被接受。

除了上述问题,在测试inlineRegExpstoredRegExp时,你所看到的代码只在源代码文本被解析时初始化一次,而对于dynamicRegExp,每次调用该方法都会创建一个新的正则表达式。请注意,实际测试运行类似于r = dynamicRegExp(element)这样的代码多次,而准备代码只运行一次。

根据另一个jsPerf的结果,下面的代码给出了大致相同的结果:

var reContains = /(?:^| )foo(?: |$)/;

...and

var reContains = RegExp('(?:^| )foo(?: |$)'); 
抱歉,我没有理解您的请求。您可以提供更多上下文或详细说明吗?
function storedRegExp(node) {
  return reContains.test(node.className);
}

当然,RegExp('(?:^| )foo(?: |$)') 的源代码可能会首先被解析为一个 String,然后再解析成RegExp,但我怀疑仅此而已不会使其变慢两倍。然而,以下代码将为每个方法调用创建一个新的 RegExp(..)一遍又一遍

function dynamicRegExp(node) {
  return RegExp('(?:^| )foo(?: |$)').test(node.className);
}

如果在原始测试中,您只调用每个方法一次,则内联版本的速度不会快2倍。

(我更惊讶于inlineRegExpstoredRegExp产生不同的结果。这也在 Alexander's answer 中解释了。)


1
这真的很有道理,谢谢。你可能想要强调一下你的jsperf,因为它帮助我理解了dynamicStoredRegExpdynamicRegExp之间性能差异的原因。至于你最后的陈述,那并没有太大的区别,我会忽略它。 - aaaaaa
关于inlineRegExpstoredRegExp之间的区别:在最新版本的Safari 6.0中,前者速度只有后者的一半。详见此链接。‽ - Arjan
1
在Safari 6.0上,我没有看到,storedRegExpdynamicStoredRegExp的速度大约是inlineRegExp的两倍,而在其他浏览器上它们几乎相同。现在我也很好奇Safari可能会发生什么。 - aaaaaa
1
@Edward,几乎正确。/pattern/RegExp('pattern') 都会创建一个新的对象,就像你可以在 Alexander 的回答中读到的那样。但是使用类似 /pattern/ 这样的字面量而不是字符串可以确保正则表达式本身只需要编译一次。因此,现在 /pattern/RegExp(/pattern/) 实际上是相同的,但对于 RegExp('pattern'),正则表达式本身每次都要被编译。 - Arjan
@Edward在一个循环中仍然会创建一个新的对象。但它不必为每个新对象编译正则表达式。但易读的代码胜出 :-) - Arjan
显示剩余3条评论

6
在第二种情况下,正则表达式对象是在语言解析期间创建的,而在第一种情况下,RegExp类构造函数必须解析任意字符串。

你的意思是在第一种情况下,正则表达式在被引擎“理解”之前会经历一种“字符串”状态? - aaaaaa
1
哦,更好的是,告诉我是否正确:斜杠作为正则表达式定界符,因此斜杠意味着正则表达式,就像引号意味着字符串一样? - aaaaaa

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