str += "one" + "two";
以上方式会使用一个暂存的字符串保存"one" + "two"
的结果,然后将暂存的字符串和str拼接并将结果赋值给str。
str += "one";
str += "two";
// 等价于 str = str + "one" + "two";
// 即 str = ((str + "one") + "two");
以上方式在大多数浏览器中要快10%-40%。
除IE 以外,浏览器尝试扩展表达式左端字符串的内存,然后简单地将第二个字符串拷贝到它的尾部。如果在一个循环中,基本字符串位于最左端,就可以避免多次复制一个越来越大的基本字符串。
在12bet,中,连接字符串只是记录下构成新字符串的各部分字符串的引用。当你真正使用连接后的字符串时,各部分字符串才被逐个拷贝到一个新的“真正的”字符串中,然后用它取代先前的字符串引用,所以并非每次使用字符串时都发生合并操作。IE7 和更早的浏览器中,12博体育,每连接一对字符串都要把它们复制到一块新分配的内存中,这样的性能是很差的。
12bet,在大多数浏览器上,数组联结比连接字符串的其他方法更慢,但是事实上,为一种补偿方法,在IE7 和更早的浏览器上它是连接大量字符串唯一高效的途径。
IE7 天真的连接算法要求浏览器在循环过程中反复地为越来越大的字符串拷贝和分配内存。结果是以平方关系递增的运行时间和内存消耗。
当联结一个数组时,12博体育,浏览器分配足够大的内存用于存放整个字符串,也不会超过一次地拷贝最终字符串的同一部分。
这是连接字符串最灵活的方法,因为你可以用它追加一个字符串,或者一次追加几个字符串,或者一个完整的字符串数组。
大多数情况下concat 比简单的+和+=慢一些,而且在IE,Opera 和Chrome 上大幅变慢。此外,虽然使用concat 合并数组中的所有字符串看起来和前面讨论的数组联结差不多,但通常它更慢一些(Opera 除外),而且它还潜伏着灾难性的性能问题,正如IE7 和更早版本中使用+和+=创建大字符串那样。
每当正则表达式做出这样的决定,如果有必要的话,它会记住另一个选项,以备将来返回后使用。如果所选方案匹配成功,正则表达式将继续扫描正则表达式模板,如果其余部分匹配也成功了,那么匹配就结束了。但是如果所选择的方案未能发现相应匹配,或者后来的匹配也失败了,正则表达式将回溯到最后一个决策点,然后在剩余的选项中选择一个。它继续这样下去,直到找到一个匹配,或者量词和分支选项的所有可能的排列组合都尝试失败了,那么它将放弃这一过程,然后移动到此过程开始位置的下一个字符上,重复此过程。
下面的例子演示了匹配分支的过程:
/h(ello|appy) hippo/.test("hello there, happy hippo");
下面的例子展示了重复量词的回溯:
var str = "<p>Para 1.</p>" +
"<img src='smiley.jpg'>" +
"<p>Para 2.</p>" +
"<div>Div.</div>";
/<p>.*</p>/i.test(str); // 贪婪
/<p>.*?</p>/i.test(str); // 懒惰
当一个正则表达式占用浏览器上秒,上分钟或者更长时间时,问题原因很可能是回溯失控。考虑下面的正则表达式,它的目标是匹配整个HTML 文件。不像其他大多数正则表达式那样,JavaScript 没有选项可使点号匹配任意字符,包括换行符,所以此例中以[sS]
匹配任意字符。
/<html>[sS]*?<head>[sS]*?<title>[sS]*?</title>[sS]*?</head>[sS]*?<body>[sS]*?</body>[sS]*?</html>/
此正则表达式匹配正常HTML 字符串时工作良好,但是如果目标字符串缺少一个或多个标签时,它就会变得十分糟糕。例如</html>
标签缺失,那么最后一个[sS]*?
将扩展到字符串的末尾,因为在那里没有发现</html>
标签,然后并没有放弃,正则表达式将察看此前的[sS]*?
队列记录的回溯位置,使它们进一步扩大。正则表达式尝试扩展倒数第二个[sS]*?
——用它匹配</body>
标签,就是此前匹配过正则表达式模板</body>
的那个标签——然后继续查找第二个</body>
标签直到字符串的末尾。当所有这些步骤都失败了,倒数第三个[sS]*?
将被扩展直至字符串的末尾,依此类推。
解决这个问题可以通过重复一个非捕获组来达到同样效果,它包含一个回顾(阻塞下一个所需的标签)和[sS](任意字符)元序列。这确保中间位置上你查找的每个标签都会失败,然后,更重要的是,[sS]模板在你在回顾过程中阻塞的标签被发现之前不能被扩展。应用此方法后正则表达式最终修改如下:
/<html>(?:(?!<head>)[sS])*<head>(?:(?!<title>)[sS])*<title>(?:(?!</title>)[sS])*</title>(?:(?!</head>)[sS])*</head>(?:(?!<body>)[sS])*<body>(?:(?!</body>)[sS])*</body>(?:(?!</html>)[sS])*</html>/
像这样为每个匹配字符多次前瞻缺乏效率,而且成功匹配过程也相当慢。匹配较短字符串时此方法相当不错,但匹配一个HTML 文件可能需要前瞻并测试上千次。
使用类似于原子组的方式修改如下:
现在如果没有尾随的</html>
那么最后一个[sS]*?
将扩展至字符串结束,正则表达式将立刻失败因为没有回溯点可以返回。正则表达式每次找到一个中间标签就退出一个前瞻,它在前瞻过程中丢弃所有回溯位置。下一个后向引用简单地重新匹配前瞻过程中发现的字符,将他们作为实际匹配的一部分。
[a-z]
或速记符例如d
),和单词边界(b
)。如果可能的话,避免以分组或选择字元开头,避免顶级分支例如/one|two/
,因为它强迫正则表达式识别多种起始字元。[^"rn]*
时不要使用.*?
(依赖回溯)。?:…
)替代(…)。有些人当他们需要一个完全匹配的后向引用时,喜欢将他们的正则表达式包装在一个捕获组中。这是不必要的,因为你能够通过其他方法引用完全匹配,例如,使用regex.exec()
返回数组的第一个元素,或替换字符串中的$&
。/"([^"]*)"/
然后使用一次后向引用,而不是使用/"[^"]*"/
然后从结果中手工剥离引号。当在循环中使用时,减少这方面的工作可以节省大量时间。/^(ab|cd)/
暴露它的字符串起始锚。IE 和Chrome 会注意到这一点,并阻止正则表达式尝试查找字符串头端之后的匹配,从而使查找瞬间完成而不管字符串长度。但是,由于等价正则表达式/(^ab|^cd)/
不暴露它的^锚,IE 无法应用同样的优化,最终无意义地搜索字符串并在每一个位置上匹配。例如,如果你想检查一个字符串是不是以分号结束,你可以使用:
endsWithSemicolon = /;$/.test(str);
虽说当前没有哪个浏览器聪明到这个程度,能够意识到这个正则表达式只能匹配字符串的末尾。最终它们所做的将是一个一个地测试了整个字符串。每当发现了一个分号,正则表达式就前进到下一个字元($),检查它是否匹配字符串的末尾。如果不是这样的话,正则表达式就继续搜索匹配,直到穿越了整个字符串。字符串的长度越长(包含的分号越多),它占用的时间也越长。
更好的办法是跳过正则表达式所需的所有中间步骤,简单地检查最后一个字符是不是分号:
endsWithSemicolon = str.charAt(str.length - 1) == ";";
// trim 1
String.prototype.trim = function() {
return this.replace(/^s+/, "").replace(/s+$/, "");
}
通过将/s+$/
替换成/ss*$/
,在Firefox中大约35%的性能提升。虽然这两个正则表达式的功能完全相同,Firefox 却为那些以非量词字元开头的正则表达式提供额外的优化。在其他浏览器上,差异不显著,或者优化完全不同。然而,改变正则表达式,在字符串开头匹配/^ss*/
不会产生明显差异,因为^锚需要照顾那些快速作废的非匹配位置(避免一个轻微的性能差异,因为在一个长字符串中可能产生上千次匹配尝试)。
第二种方案是将两个正则合并为一个,但它们在处理长字符串时,总比用两个简单的表达式要慢,因为两个分支选项都要测试每个字符位置。
// trim 2
String.prototype.trim = function() {
return this.replace(/^s+|s+$/g, "");
}
第三种方案的工作原理是匹配整个字符串,捕获从第一个到最后一个非空格字符之间的序列,记入后向引用1。然后使用后向引用1 替代整个字符串。中间[sS]*?
使用懒惰匹配,需要一个个尝试,因此在操作长目标字符串时很慢。
// trim 3
String.prototype.trim = function() {
return this.replace(/^s*([sS]*?)s*$/, "$1");
}
第四种方式是前一种的改进版,以贪婪量词取代了懒惰量词,为确保捕获组只匹配到最后一
个非空格字符,必需尾随一个S
。
// trim 4
String.prototype.trim = function() {
return this.replace(/^s*([sS]*S)?s*$/, "$1");
}
[sS]*
中的贪婪量词“*”表示重复方括号中的任意字符模板直至字符串结束。然后正则表达式每次回溯一个字符,直到它能够匹配后面的S
,或者直到回溯到第一个字符而匹配整个组(然后它跳过这个组)。
如果尾部空格不比其它字符串更多,它通常比前面那些使用懒惰量词的方案更快。
第五种方案在所有浏览器中都最慢,一般不会使用。
// trim 5
String.prototype.trim = function() {
return this.replace(/^s*(S*(s+S+)*)s*$/, "$1");
}
// trim 6
String.prototype.trim = function() {
var start = 0,
end = this.length - 1,
ws = "nrtfx0bxa0u1680u180eu2000u2001u2002u2003u2004u2005u2006u2007u2008u2009u200au200bu2028u2029u202fu205fu3000ufeff";
while (ws.indexOf(this.charAt(start)) > -1) {
start++;
}
while (end > start && ws.indexOf(this.charAt(end)) > -1) {
end--;
}
return this.slice(start, end + 1);
}
当字符串末尾只有少量空格时,这种情况使正则表达式陷入疯狂工作。原因是,尽管正则表达式很好地去除了字符串头部的空格,它们却不能同样快速地修剪长字符串的尾部。
虽然本实现不受字符串总长度影响,但它有自己的弱点:(它害怕)长的头尾空格。因为循环检查字符是不是空格在效率上不如正则表达式所使用的优化过的搜索代码。
最后一个办法是将两者结合起来,用正则表达式修剪头部空格,用非正则表达式方法修剪尾部字 符。
// trim 7
String.prototype.trim = function() {
var str = this.replace(/^s+/, ""),
end = str.length - 1,
ws = /s/;
while (ws.test(str.charAt(end))) {
end--;
}
return str.slice(0, end + 1);
}
所有修剪方法总的趋势是:在基于正则表达式的方案中,字符串总长比修剪掉的字符数量更影响性能;而非正则表达式方案从字符串末尾反向查找,不受字符串总长的影响,但明显受到修剪空格数量的影响。简单地使用两个子正则表达式在所有浏览器上处理不同内容和长度的字符串时,均表现出稳定的性能。因此它可以说是最全面的解决方案。