replaceAll的性能秘密是什么?[HTML转义]

10

我花了一些时间寻找转义HTML字符串的最佳方法,并在以下讨论中发现了一些有用的信息:讨论1讨论2。这引导我使用了replaceAll函数。但之后我进行了性能测试,尝试找到相似速度的解决方案却没有成功。

这里是我最终的测试案例集。我在网络上找到它并扩展了我的尝试(底部的4个案例),但仍无法达到replaceAll()的速度。

是什么秘密使得replaceAll()的解决方案如此迅速?

问候!

代码片段:

String.prototype.replaceAll = function(str1, str2, ignore) 
{
   return this.replace(new RegExp(str1.replace(/([\/\,\!\\\^\$\{\}\[\]\(\)\.\*\+\?\|\<\>\-\&])/g,"\\$&"),(ignore?"gi":"g")),(typeof(str2)=="string")?str2.replace(/\$/g,"$$$$"):str2);
};

感谢qwerty

到目前为止,最快的情况:

html.replaceAll('&', '&amp;').replaceAll('"', '&quot;').replaceAll("'", '&#39;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');

2
许多内置方法是使用本机代码实施并进行了预优化(其中包括正则表达式),在Javascript中以更快的方式模拟它们纯粹是很难做到的。 - Joachim Isaksson
当然,但为什么“replace new RegExp”案例如此缓慢。它也使用了RegExp。 - Saram
仍然使用替换而不是正则表达式似乎更快。http://jsperf.com/replaceallvssplitjoin - Mr_Green
1
@Mr_Green 多重替换是错误的,因为它只会替换第一次出现的 :) - Ja͢ck
3
始终编译您的正则表达式;http://jsperf.com/htmlencoderegex/32 - Joachim Isaksson
显示剩余6条评论
3个回答

4

终于找到了它!感谢Jack指出了jsperf的特定问题。

我应该注意到测试结果很奇怪,当.replaceAll()在Benchmark.prototype.setup内定义时,与全局定义(即在标签内部)相比,它运行速度快两倍。我仍然不确定为什么会这样,但肯定与jsperf本身的工作方式有关。

答案是:

replaceAll - 这超过了jsperf的限制/错误,由特殊序列"\\$&"引起,因此结果是错误的。

compile() - 当没有参数调用时,它将正则表达式定义更改为/(?:)。我不知道这是否是一个错误或者其他原因,但在它被调用后性能结果很差。

这是我的安全测试结果

最后,我准备了正确的测试用例

结果是,对于HTML转义,最好使用基于本机DOM的解决方案,例如:

document.createElement('div').appendChild(document.createTextNode(html)).parentNode.innerHTML

或者如果你重复多次使用它,可以通过一次准备好的变量来完成:

//prepare variables
var DOMtext = document.createTextNode("test");
var DOMnative = document.createElement("span");
DOMnative.appendChild(DOMtext);

//main work for each case
function HTMLescape(html){
  DOMtext.nodeValue = html;
  return DOMnative.innerHTML
}

感谢您所有人的合作和留言指导。 jsperf错误描述 String.prototype.replaceAll被定义为下列内容:
function (str1, str2, ignore) {
  return this.replace(new RegExp(str1.replace(repAll, "\\#{setup}"), (ignore ? "gi" : "g")), (typeof(str2) == "string") ? str2.replace(/\$/g, "$$") : str2);
}

1
你能提供 jsperf bug 的链接吗? - Ja͢ck
@Jack,当你开始任何有缺陷的测试(其中replaceAll在setup()过程中定义)时,请转到控制台并显示String.prototype.replaceAll的主体。我会在一分钟内回答一些评论。 - Saram

2
就性能而言,我认为以下函数是最好的:
String.prototype.htmlEscape = function() {
    var amp_re = /&/g, sq_re = /'/g, quot_re = /"/g, lt_re = /</g, gt_re = />/g;

    return function() {
        return this
          .replace(amp_re, '&amp;')
          .replace(sq_re, '&#39;')
          .replace(quot_re, '&quot;')
          .replace(lt_re, '&lt;')
          .replace(gt_re, '&gt;');
    }
}();

它初始化正则表达式并返回一个执行替换的闭包。

性能测试

我应该注意到,测试结果很奇怪;当 .replaceAll()Benchmark.prototype.setup 内部定义时,与在全局定义(即在 <script> 标签内部)相比,它运行速度快了两倍。我仍然不确定为什么,但它肯定与 jsperf 自身的工作方式有关。

使用 RegExp.compile()

我想避免使用已弃用的函数,主要是因为现代浏览器应该自动完成这种性能。这里是一个带编译表达式的版本:

String.prototype.htmlEscape2 = function() {
    var amp_re = /&/g, sq_re = /'/g, quot_re = /"/g, lt_re = /</g, gt_re = />/g;

    if (RegExp.prototype.compile) {
        amp_re.compile();
        sq_re.compile();
        quot_re.compile();
        lt_re.compile();
        gt_re.compile();
    }

    return function() {
        return this
          .replace(amp_re, '&amp;')
          .replace(sq_re, '&#39;')
          .replace(quot_re, '&quot;')
          .replace(lt_re, '&lt;')
          .replace(gt_re, '&gt;');
    }
}

这段文字的翻译如下:

这样做会让其他所有事情都相形见绌!

性能测试

.compile() 之所以能够带来如此大的性能提升,是因为当你编译一个全局表达式时,比如 /a/g,它会被转换成 /(?:)/(在 Chrome 上),从而变得无用。

如果编译不能完成,浏览器应该抛出错误,而不是默默地销毁它。


看一下 @Joachim Isaksson 上面的评论。我认为他找到了诀窍。 - Saram
@Saram 我没有使用已弃用的.compile()进行测试,但我怀疑那不会是一个有用的统计数据。测试基准中的某些因素正在影响结果。 - Ja͢ck
@Saram 另外,根据 MDN,字面正则表达式确实是被编译的。 - Ja͢ck
@Saram 我已经更新了编译版本的代码;我仍然认为浏览器应该对这些内容进行优化。 - Ja͢ck
我发现compile()会破坏正则表达式,因此不能使用。看起来像是一个bug。 - Saram

-1

实际上有更快的方法来做这件事。

如果您可以进行内联分割和连接,您将获得更好的性能。

//example below
var test = "This is a test string";
var test2 = test.split("a").join("A");

尝试这个并运行性能测试。


使用replaceAll函数:0.054秒/ 使用replaceAll2函数:0.106秒/ 内联分割/连接:0.111秒/ 内联RegExp对象:0.182秒 无字符串内联RegExp对象:0.134 - Saram

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