建议48:慎用正则表达式修剪字符串
(1)使用两个子表达式修剪字符串
去除字符串首尾的空格是一个简单而常见的任务,但到目前为止JavaScript 还没有实现它。正则表达式允许用很少的代码实现一个修剪函数,最好的全面解决方案可能是使用两个子表达式:一个用于去除头部空格,另一个用于去除尾部空格。这样处理简单而快速,特别是处理长字符串时。
if(!String.prototype.trim) {
String.prototype.trim = function() {
return this.replace(/^\s+/, "").replace(/\s+$/, "");
}
}
var str = " tn test string ".trim();
alert(str == "test string"); // alerts "true"
使用if语句进行检测,如果已经存在trim原生函数,则不要覆盖trim原生函数,因为原生函数进行了优化后通常远远快于自定义函数。使用上面代码在Firefox浏览器中大约有35%的性能提升(或多或少依赖于目标字符串的长度和内容)。将/s+$/(第二个正则表达式)替换成/ss*$/。虽然这两个正则表达式的功能完全相同,但是Firefox浏览器却为那些以非量词字元开头的正则表达式提供额外的优化。在其他浏览器上,差异不显著,或者优化完全不同。
然而,改变正则表达式,在字符串开头匹配/^ss*/不会产生明显差异,因为^锚需要“照顾”那些快速作废的非匹配位置(避免一个轻微的性能差异,因为在一个长字符串中可能产生上千次匹配尝试)。
(2)使用一个正则表达式修剪字符串
事实上,除这里列出的方法外还有许多其他方法,可以写一个正则表达式来修剪字符串,但在处理长字符串时,这种方法执行速度总比用两个简单的表达式要慢。
String.prototype.trim = function() {
return this.replace(/^\s+|\s+$/g, "");
}
这可能是最通常的解决方案。它通过分支功能合并了两个简单的正则表达式,并使用/g(全局)标记替换所有匹配,而不只是第一个匹配(当目标字符串首尾都有空格时将匹配两次)。这并不是一个“可怕”的方法,但在对长字符串操作时,它比使用两个简单的子表达式要慢,因为两个分支选项都要测试每个字符位置。
String.prototype.trim = function() {
return this.replace(/^\s*([\s\S]*?)\s*$/, "$1");
}
这个正则表达式的工作原理是匹配整个字符串,捕获从第一个到最后一个非空格字符之间的序列,记入后向引用1。然后使用后向引用1 替代整个字符串,就留下了这个字符串的修剪版本。
这个方法概念简单,但捕获组中的“懒惰”量词使正则表达式进行了许多额外操作(如回溯),因此在操作长目标字符串时很慢。在进入正则表达式捕获组时,[sS]类的“懒惰”量词?要求捕获组尽可能地减少重复次数。因此,这个正则表达式每匹配一个字符,都要停下来尝试匹配余下的s$模板。如果由于字符串当前位置之后存在非空格字符而导致匹配失败,正则表达式将匹配一个或多个字符,更新后向引用,然后再次尝试匹配模板的剩余部分。
String.prototype.trim = function() {
return this.replace(/^\s*([\s\S]*\S)?\s*$/, "$1");
}
这个表达式与上一个很像,但出于性能原因以“贪婪”量词取代了“懒惰”量词。为确保捕获组只匹配到最后一个非空格字符,必须尾随一个S。然而,由于正则表达式必须匹配全部由空格组成的字符串,整个捕获组通过尾随一个?量词而成为可选组。
在此,[sS]中的“贪婪”量词“”表示重复方括号中的任意字符模板直至字符串结束。然后,正则表达式每次回溯一个字符,直到它能够匹配后面的S,或者直到回溯到第一个字符而匹配整个组(之后它跳过这个组)。
如果尾部空格不比其他字符串更多,通过一个表达修剪的方案通常比前面那些使用“懒惰”量词的方案更快。事实上,这个方案在IE、Safari、Chrome和Opera浏览器上执行速度如此之快,甚至超过使用两个子表达式的方案,是因为这些浏览器包含特殊优化,专门服务于为字符类匹配任意字符的“贪婪”重复操作,正则表达式引擎直接跳到字符串末尾而不检查中间的字符(尽管回溯点必须被记下来),然后适当回溯。不幸的是,这种方法在Firefox 和Opera 9 浏览器上执行得非常慢,所以到目前为止,使用两个子表达式仍然是更好的跨浏览器方案。
String.prototype.trim = function() {
return this.replace(/^\s*(\S*(\s+\S+)*)\s*$/, "$1");
}
这是一个相当普遍的方法,但没有很好的理由使用它,因为它在所有浏览器上都是这里所列出的所有方法中执行得最慢的一个。这类似于最后两个正则表达式,它匹配整个字符串然后用打算保留的部分替换这个字符串,因为内部组每次只匹配一个单词,正则表达式必须执行大量的离散步骤。修剪短字符串时性能冲击并不明显,但处理包含多个词的长字符串时,这个正则表达式可以成为影响性能的一个问题。
将内部组修改为一个非捕获组,例如,将(s+S+)修改为(?:s+S+),在Opera、IE和Chrome 浏览器上缩减了大约20%~45%的处理时间,在Safari 和Firefox 浏览器上也有轻微改善。尽管如此,一个非捕获组不能完全代换这个实现。注意,外部组不能转换为非捕获组,因为它在被替换的字符串中被引用了。
虽然正则表达式的执行速度很快,但是没有它们帮助时修剪字符串的性能还是值得考虑的。例如:
String.prototype.trim = function() {
var start = 0,
end = this.length - 1,
ws = " \n\r\t\f\x0b\xa0\u1680\u180e\u2000\u2001\u2002\u2003
\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u202f
\u205f\u3000\ufeff";
while (ws.indexOf(this.charAt(start)) > -1) {
start++;
}
while (end > start && ws.indexOf(this.charAt(end)) > -1) {
end--;
}
return this.slice(start, end + 1);
}
在上面代码中,ws变量包括在ECMAScript v5中定义的所有空白字符。出于效率方面的考虑,在得到修剪区的起始和终止位置之前避免复制字符串的任何部分。
当字符串末尾只有少量空格时,这种情况使正则表达式处于无序状态。原因是,尽管正则表达式很好地去除了字符串头部的空格,却不能同样快速地修剪长字符串的尾部。一个正则表达式不能跳到字符串的末尾而不考虑沿途字符。正因如此,在第二个while循环中从字符串末尾向前查找一个非空格字符。
虽然上面代码不受字符串总长度影响,但是它有自己的弱点—长的头尾空格,因为循环检查字符是不是空格在效率上不如正则表达式所使用的优化过的搜索代码。
(3)正则表达式与非正则表达式结合起来修剪字符串
最后一个办法是将正则表达式与非正则表达式两者结合起来,用正则表达式修剪头部空格,用非正则表达式方法修剪尾部字符。
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);
}
当只修剪一个空格时,此混合方法非常快,同时去除了性能上的风险,如以长空格开头的字符串,完全由空格组成的字符串(尽管它在处理尾部长空格的字符串时仍具有弱点)。
注意:此方案在循环中使用正则表达式检测字符串尾部的字符是否为空格,虽然使用正则表达式增加了一点性能负担,但是它允许根据浏览器定义空格字符列表,以保持简短和兼容性。
所有修剪方法总的趋势:在基于正则表达式的方案中,字符串总长比修剪掉的字符数量更影响性能;而非正则表达式方案从字符串末尾反向查找,不受字符串总长的影响,但明显受到修剪空格数量的影响。简单地使用两个子正则表达式在所有浏览器上处理不同内容和长度的字符串时,均表现出稳定的性能,因此可以说这种方案是最全面的解决方案。混合解决方案在处理长字符串时特别快,其代价是代码稍长,在某些浏览器上处理尾部长空格时存在弱点。