几乎所有JavaScript 程序都与字符串操作紧密相连。例如,许多应用程序使用Ajax从服务器获取字符串,将这些字符串转换成更易用的JavaScript对象,然后从数据中生成HTML字符串。一个典型的程序需要处理若干这样的任务,合并,分解,重新排列,搜索,遍历,以及其他方法处理字符串。随着网页应用越来越复杂,越来越多的此类任务将在浏览器中完成。
字符串连接表现出惊人的性能紧张。通常一个任务通过一个循环,向字符串末尾不断地添加内容,来创建一个字符串(例如,创建一个HTML 表或者一个XML 文档),但此类处理在一些浏览器上表现糟糕而遭人痛恨。
当连接少量字符串时,所有这些函数都很快,临时使用的话,可选择最熟悉的使用。当合并字符串的长度和数量增加之后,有些函数开始显示出自己的威力。
一、加和加等于操作
这些操作符提供了连接字符串的最简单方法,事实上,除IE7 和它之前的所有现代浏览器都对此优化得很好,所以你不需要寻找其他方法。然而,有些技术可以最大限度地提高这些操作的效率。
首先,看一个例子。这是连接字符串的常用方法:
str += "one" + "two";
此代码执行时,发生四个步骤:
1. 内存中创建了一个临时字符串。
2. 临时字符串的值被赋予“onetwo”。
3. 临时字符串与str 的值进行连接。
4. 结果赋予str。
这基本上就是浏览器完成这一任务的过程。
下面的代码通过两个离散表达式直接将内容附加在str上避免了临时字符串(上面列表中第1步和第2步)。在大多数浏览器上这样做可加快10%-40%:
str += "one";
str += "two";
实际上,你可以用一行代码就实现这样的性能提升,如下:
str = str + "one" + "two";
// equivalent to str = ((str + "one") + "two")
这就避免了使用临时字符串,因为赋值表达式开头以str 为基础,一次追加一个字符串,从左至右依次连接。如果改变连接顺序(例如,str = "one" + str + "two"),你会失去这种优化。这与浏览器合并字符串时分配内存的方法有关。除IE 以外,浏览器尝试扩展表达式左端字符串的内存,然后简单地将第二个字符串拷贝到它的尾部。如果在一个循环中,基本字符串位于最左端,就可以避免多次复制一个越来越大的基本字符串。
这些技术并不适用于IE。它们几乎没有任何作用,在IE8 上甚至比IE7 和早期版本更慢。这与IE执行连接操作的机制有关。在IE8 中,连接字符串只是记录下构成新字符串的各部分字符串的引用。在最后时刻(当你真正使用连接后的字符串时),各部分字符串才被逐个拷贝到一个新的“真正的”字符串中,然后用它取代先前的字符串引用,所以并非每次使用字符串时都发生合并操作。
IE7和更早的浏览器在连接字符串时使用更糟糕的实现方法,每连接一对字符串都要把它们复制到一块新分配的内存中。你会在后面的“数组联结”一节中看到它潜在的巨大影响。针对IE8 之前的实现方式,本节的建议反而会使代码更慢,因为合并多个短字符串比连接一个大字符串更快(避免多次拷贝那些大字符串)。例如,largeStr = largeStr + s1 + s2 语句,在IE7 和更早的版本中,必须将这个大字符串拷贝两次,首先与s1 合并,然后再与s2 合并。相反,largeStr = s1 + s2 首先将两个小字符串合并起来,然后将结果返回给大字符串。创建中间字符串s1 + s2 与两次拷贝大字符串相比,性能冲击要轻得多。
二、 Firefox和编译期合并
在赋值表达式中所有字符串连接都属于编译期常量,Firefox 自动地在编译过程中合并它们。这里有一个方法可看到这一过程:
function foldingDemo() {
var str = "compile" + "time" + "folding";
str += "this" + "works" + "too";
str = str + "but" + "not" + "this";
}
alert(foldingDemo.toString());
// In Firefox, you'll see this:
// function foldingDemo() {
// var str = "compiletimefolding";
// str += "thisworkstoo";
// str = str + "but" + "not" + "this";
// }
当字符串是这样合并在一起时,由于运行时没有中间字符串,所以连接它们的时间和内存可以减少到零。这种功能非常了不起,但它并不经常起作用,因为通常从运行期数据创建字符串而不是从编译期常量。
三、数组联结
Array.prototype.join 方法将数组的所有元素合并成一个字符串,并在每个元素之间插入一个分隔符字符
串。如果传递一个空字符串作为分隔符,你可以简单地将数组的所有元素连接起来。
在大多数浏览器上,数组联结比连接字符串的其他方法更慢,但是事实上,为一种补偿方法,在IE7和更早的浏览器上它是连接大量字符串唯一高效的途径。
下面的示例代码演示了可用数组联结解决的性能问题:
var str = "I'm a thirty-five character string.",
newStr = "",
appends = 5000;
while (appends–) {
newStr += str;
}
此代码连接5'000 个长度为35 的字符串。图5-2 显示出在IE7 中执行此测试所需的时间,从5'000 次连接开始,然后逐步增加连接数量。
IE7天真的连接算法要求浏览器在循环过程中反复地为越来越大的字符串拷贝和分配内存。结果是以平方关系递增的运行时间和内存消耗。
好消息是所有其他的现代浏览器(包括IE8)在这个测试中表现良好,不会呈现平方关系的复杂性递增,这是真正的杀手级改善。然而,此程序演示了看似简单的字符串连接所产生的影响。5'000 次连接用去226毫秒已经是一个显著的性能冲击了,应当尽可能地缩减这一时间,但锁定用户浏览器长达3 秒,只是为了连接20'000 个短字符串,则对任何应用程序来说都是不能接受的。
现在考虑下面的测试,它使用数组联结生成同样的字符串:
var str = "I'm a thirty-five character string.",
strs = [],
newStr,
appends = 5000;
while (appends–) {
strs[strs.length] = str;
}
newStr = strs.join("");
这一难以置信的改进结果是因为避免了重复的内存分配和拷贝越来越大的字符串。当联结一个数组时,浏览器分配足够大的内存用于存放整个字符串,也不会超过一次地拷贝最终字符串的同一部分。
原生字符串连接函数接受任意数目的参数,并将每一个参数都追加在调用函数的字符串上。这是连接字符串最灵活的方法,因为你可以用它追加一个字符串,或者一次追加几个字符串,或者一个完整的字符串数组。
// append one string
str = str.concat(s1);
// append three strings
str = str.concat(s1, s2, s3);
// append every string in an array by using the array
// as the list of arguments
str = String.prototype.concat.apply(str, array);
不幸的是,大多数情况下concat比简单的+和+=慢一些,而且在IE,Opera和Chrome上大幅变慢。此外,虽然使用concat 合并数组中的所有字符串看起来和前面讨论的数组联结差不多,但通常它更慢一些(Opera除外),而且它还潜伏着灾难性的性能问题,正如IE7和更早版本中使用+和+=创建大字符串那样。