讀古今文學網 > 編寫高質量代碼:改善JavaScript程序的188個建議 > 建議36:警惕字符串連接操作 >

建議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和更早版本中使用「+」和「+=」創建大字符串情況類似。