首页 » 编写高质量代码:改善JavaScript程序的188个建议 » 编写高质量代码:改善JavaScript程序的188个建议全文在线阅读

《编写高质量代码:改善JavaScript程序的188个建议》建议36:警惕字符串连接操作

关灯直达底部

字符串连接表现出惊人的“性能紧张”。一个任务通过一个循环向字符串末尾不断地添加内容,以创建一个字符串。例如,创建一个HTML表或一个XML文档。此类处理在一些浏览器上表现得非常糟糕。

当连接少量字符串时,这些问题都可以忽略,临时使用可选择最熟悉的操作。当合并字符串的长度和数量增加之后,有些函数开始显示出“威力”。

(1)+、+=

+、+=运算符提供了连接字符串的最简单方法。除IE 7及其以前版本外,当前所有浏览器都对这种方法优化得很好,因此不需要使用其他方法。当然,还可以提高这些操作的效率。例如,下面这行代码是字符串连接的常用方法:


str+=/"one/"+/"two/";


JavaScript在执行这行代码时,会进行以下4个步骤:

第1步,在内存中创建一个临时字符串。

第2步,临时字符串的值被赋予“onetwo”。

第3步,临时字符串与str的值进行连接。

第4步,把结果赋予str。

不过,通过下面代码进行优化能够提高执行效率:两个离散表达式直接将内容附加到str上,避免了临时字符串(第1步和第2步)。在大多数浏览器中,这样做可以使执行速度提升10%~40%。


str+=/"one/";

str+=/"two/";


实际上,也可以用以下一行代码实现同样的性能提升。


str=str+/"one/"+/"two/";


这就避免了使用临时字符串,因为赋值表达式开头以str为基础,一次追加一个字符串,从左至右依次连接。如果改变连接顺序,如下所示:


str=/"one/"+str+/"two/";


就会失去这种优化性能。这与浏览器合并字符串时分配内存的方法有关。除IE以外,浏览器尝试扩展表达式左端字符串的内存,然后简单地将第二个字符串复制到它的尾部。在一个循环中,如果基本字符串位于最左端,就可以避免多次复制一个越来越大的基本字符串。

然而,上面的方法并不适用于IE。对于IE来说,这种优化几乎没有任何作用,在IE 8上甚至比IE 7和早期版本更慢,这与IE执行连接操作的机制有关。

在IE 8中,连接字符串只是记录下构成新字符串的各部分字符串的引用。在最后时刻,各部分字符串才被逐个复制到一个新的“真正的”字符串中,然后用它取代先前的字符串引用,因此并非每次使用字符串时都发生合并操作。

IE 7和更早的浏览器在连接字符串时使用更糟糕的实现方法,每连接一对字符串都要将其复制到一块新分配的内存中。使用上述方法反而会使代码执行速度更慢,因为合并多个短字符串比连接一个大字符串更快,因此要避免多次复制那些大字符串。例如:


largeStr=largeStr+s1+s2;


在IE 7和更早的版本中,必须将这个大字符串复制两次。首先与s1合并,然后再与s2合并。相反,对于下面代码:


largeStr=s1+s2;


先将两个小字符串合并起来,然后将结果返回给大字符串。创建中间字符串s1+s2与两次复制大字符串相比,对性能的“冲击”要轻得多。

(2)编译期合并

在赋值表达式中所有字符串连接都属于编译期常量,Firefox自动地在编译过程中合并它们。在以下这个方法中可看到这一过程:


function foldingDemo{

var str=/"compile/"+/"time/"+/"folding/";

str+=/"this/"+/"works/"+/"too/";

str=str+/"but/"+/"not/"+/"this/";

}

alert(foldingDemo.toString);alert(foldingDemo.toString);


在Firefox中我们经常看到这种形式:


function foldingDemo{

var str=/"compiletimefolding/";

str+=/"thisworkstoo/";

str=str+/"but/"+/"not/"+/"this/";

}

alert(foldingDemo.toString);alert(foldingDemo.toString);


当字符串是这样合并在一起时,由于运行时没有中间字符串,因此连接它们的时间和内存可以减少到零。这种功能非常了不起,但它并不经常起作用。

(3)数组联结

Array.prototype.join方法将数组的所有元素合并成一个字符串,并在每个元素之间插入一个分隔符字符串。如果传递一个空字符串作为分隔符,可以简单地将数组的所有元素连接起来。

在大多数浏览器上,数组联结比连接字符串的其他方法更慢,但事实上,作为一种补偿方法,在IE 7和更早的浏览器上它是连接大量字符串的唯一的高效途径。例如,下面的示例代码演示了可用数组联结解决的性能问题:


var str=/"I/'m a thirty-five character string./",newStr=/"/",appends=5000;

while(appends--){

newStr+=str;

}


此代码连接5000个长度为35的字符串。执行以上代码后显示在IE 7中执行此测试所需的时间,从5000次连接开始,然后逐步增加连接数量。IE 7的连接算法要求浏览器在循环过程中反复地为越来越大的字符串复制和分配内存,结果是出现以平方关系递增的运行时间和内存消耗。

目前所有其他的浏览器(包括IE 8及其以上版本)在这个测试中表现良好,不会呈现平方关系的复杂性递增,这是真正的改善。然而,此程序演示了看似简单的字符串连接所产生的影响。5000次连接用去226ms已经是一个显著的性能冲击了,应当尽可能地缩减这一时间。锁定用户浏览器长达32 s,只是为了连接20 000个短字符串,这对任何应用程序来说都是不能接受的。

如果使用数组联结生成同样的字符串,则代码如下:


var str=/"I/'m a thirty-five character string./",strs=,newStr,appends=5000;

while(appends--){

strs[strs.length]=str;

}

newStr=strs.join(/"/");


上面代码优化的核心是避免重复的内存分配和复制越来越大的字符串。当联结一个数组时,浏览器宁愿分配足够大的内存用于存放整个字符串,也不会超过一次地复制最终字符串的同一部分。

原生字符串连接函数接受任意数目的参数,并将每一个参数都追加到调用函数的字符串,这是连接字符串最灵活的方法,因为可以利用它追加一个字符串,或者一次追加几个字符串,或者追加一个完整的字符串数组。


str=str.concat(s1);

str=str.concat(s1,s2,s3);

str=String.prototype.concat.apply(str,array);


在大多数情况下,concat执行速度比简单的“+”和“+=”慢一些,而且在IE、Opera和Chrome浏览器上会大幅变慢。此外,虽然使用concat合并数组中的所有字符串看起来和前面讨论的数组联结差不多,但是通常它更慢一些(在Opera浏览器上除外),而且它还潜伏着严重的性能问题,这与在IE 7和更早版本中使用“+”和“+=”创建大字符串情况类似。