高性能JavaScript(字符串和正則表達式)


 字符串連接

+/+=操作符連接

str += "one" + "two";

這是常用的連接字符串的方法,它運行的時候會經歷下面四個步驟:

1、在內存中創建一個臨時字符串;

2、連接后的”onetwo”被賦值給這個臨時字符串;

3、臨時字符串與str的當前值連接;

4、連接后的結果賦值給str。

 

下行代碼可以避免產生臨時字符串

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

賦值表達式由 str 開始作為基礎,每次給它附加一個字符串,由左向右依次連接,因此避免了使用臨時字符串。如果改變順序 比如 把 str 放到中間 那就沒有優化的效果了。這與瀏覽器合並字符串時分配內存的方法有關。

 

數組項合並

Array.prototype.join 方法將數組的所有元素合並成一個字符串,接收一個參數作為連接符。

var str = "i am people but i very shuai a",
Newstr = "",
Appends = 5000;

while(Appends --){
Newstr += str
}

此代碼連接了5000個長度為30的字符串,下圖為IE7中完成所消耗的時間

IE7 5000次合並用了226毫秒已經明顯影響性能了,如何優化呢?

 

使用數組項合並生成通用的字符串

var str = "i am people but i very shuai a",
Strs = [],
Newstr = "",
Appends = 5000;

while(Appends --){
Strs[Strs.length] = str;
}
Newstr = Strs.join("");

IE7測試結果

由於避免了重復分配內存和拷貝逐漸增大的字符串,於是性能提升很明顯。

 

String.prototype.concat

Concat 可以接收任意數量的參數,並將每個參數附加到所調用的字符串上。

str = str.concat(s1);

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

遺憾的是,使用concat 比使用簡單的 + += 稍慢,尤其是在IE opera Chrome 慢的更明顯。

 

正則表達式優化

正則表達式工作原理,了解原理有助於更好的解決各種影響正則性能的問題。

編譯:瀏覽器驗證正則表達式對象,之后把它轉換成原生代碼程序;把正則對象賦值給一個變量,可以避免重復編譯。

設置起始位置:目標字符串的起始搜索位置,一般是字符串的起始字符、或者正則的lastIndex屬性指定位置(限於帶有/gexectest)、或者從第四步返回時的最后一次匹配的字符的下一個字符。

瀏覽器優化正則表達式引擎的辦法是,在這一階段中通過早期預測跳過一些不必要的工作。例如,如果一個正則表達式以^開頭,IE Chrome通常判斷在字符串起始位置上是否能夠匹配,然后可避免愚蠢地搜索后續位置。另一個例子是匹配第三個字母是x的字符串,一個聰明的辦法是先找到x,然后再將起始位置回溯兩個字符。

匹配每個正則表達式字元:從字符串的起始位置開始,逐個檢查文本和正則模式,當一個特定的字元匹配失敗時,回溯到之前嘗試匹配的位置,嘗試其他可能的路徑。

匹配成功或失敗:如果在當前的字符串位置有一個完全匹配,則宣布匹配成功;如果當前位置沒有所有可能的路徑都沒有成功匹配,會退回第二步,重新設置起始位置,開始另一輪匹配…直到以最后一個字符串為其實位置,仍未成功,則宣布匹配失敗。

 

理解回溯(Backtracking)

回溯是匹配過程的基本組成部分,是正則如此強大的根源,也是正則的性能消耗所在,因此如何減少回溯是提高正則的關鍵所在。回溯一般在分支和重復的情況下出現:

 

分支與回溯

/h(ello|appy) hippo/.test("hello there, happy hippo");

正則開始的h與字符串起始位置的h匹配,接下來的分支,按從左到右的原則,(ello|appy)中的ello先嘗試匹配,字符串h后面也是ello,匹配成功,於是繼續匹配正則中(ello|appy)之后的空格,仍然匹配成功,繼續匹配正則中空格之后的h,字符串空格之后是t,匹配失敗。

回到正則的分支(ello|appy)(這就是回溯),嘗試用appy對字符串第一位個字符h之后的字符進行匹配,失敗,這里沒有更多的選項,不再回溯。

第一個起始位置匹配失敗,起始位置后延一位,重新匹配h…直到字符串起始位置為14時,匹配到h

於是開啟新一輪的字元匹配,進入分支(ello|appy)中的ello,匹配失敗。

回到正則的分支(ello|appy)(再次回溯),appy匹配成功,退出分支,匹配后續的 hippo,匹配字符串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>匹配,接下來是.*(.匹配換行以外任意字符,*是貪婪量詞,表示重復0次或多次,匹配盡可能多的次數).*匹配后續一直到字符串尾部的所有字符。

嘗試匹配正則中.*后面的<,在字符串最后匹配失敗,然后每次向前回溯一個字符嘗試匹配…,直到</div>的第一個字符匹配成功,接下來正則中的\/也與字符串中的/匹配成功,繼續匹配正則中的p,匹配失敗,返回</div>,繼續向前回溯,直到第二段的</p>,匹配成功,返回<p>Para 1.</p><img src=smiley.jpg><p>Para 2.</p>,里面有2個段落和一張圖片,結束匹配。

 

回溯失控

回溯失控的時候,可能導致瀏覽器假死數秒、數分鍾或更長時間,以下面這個正則為例(用來匹配整個HTML字符串):

/<html>[\s\S]*?<head>[\s\S]*?<\/head>[\s\S]*?<body>[\s\S]*?<\/body>[\s\S]*?<\/html>/

匹配結構完整的html文件時,一切正常,但是有些標簽缺失時問題就出現了,假如html文件最后的</html>缺失,最后一個[\s\S]*?重復會擴展到字符串末尾,匹配</html>失敗,正則會依次向前搜索並記住回溯位置以便后續使用,當正則表達式擴展到倒數第二個[\s\S]*?——用它匹配由正則表達式的<\/body>匹配到的那個<body>,——然后繼續查找</body>標簽,直到字符串末尾。當所有步驟都失敗時,倒數第三個[\s\S]*?將被擴展至字符串末尾,以此類推。

回溯失控終極方案:模擬原子組(向前查看+反向引用)(?=([\s\S]*?<head>))\1

/<html>(?=([\s\S]*?<head>))\1(?=([\s\S]*?<\/head>))\2(?=([\s\S]*?<body>))\3(?=([\s\S]*?<\/body>))\4[\s\S]*?<\/html>/

原子組(向前查看)的任何回溯位置都會被丟棄,從根源上避免了回溯失控,但是向前查看不會消耗任何字符作為全局匹配的一部分,捕獲組+反向引用在這里可以用來解決這個問題,需要注意的是這的反向引用次數,即上面的\1\2\3\4對應的位置。

其它不做贅述(因為我理解不了了)。

 

何時不使用正則表達式

如果僅僅是搜索字符串,而且事先知道字符串的哪部分需要被測試時,正則並不是最佳的解決方案。比如,檢查一個字符串是否以分號結尾:

/;$/.test(str);正則會從第一個字符開始,逐個測試整個字符串,看她是否是分號,在判斷是否在字符串的最后,當字符串很長時,需要的時間越多。

str.charAt(str.length 1) == ;;這個直接跳到最后一個字符,檢查是否為分號,字符串很小是可能只是快一點點,但是對於長字符串,長度不會影響所需的時間。

字符串的原生方法都是很快的,比如slicesubstrsubstringindexOflastIndexOf等,他們可以避免正則帶來的性能開銷。

 

去除字符串首尾空白

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);
}

這個解決方案用正則來去除頭部的空白,位置錨^,會很快,主要是尾部的空白處理,像上面何時不使用正則表達式里說的,用正則並不是最佳的,這里用字符串原生方法結合正則來解決,可以避免性能受到字符串長度和空白的長度的影響。

 

小結:

當連接數量巨大或尺寸巨大的字符串時,數組項合並是唯一在IE7及更早版本中性能合理的方法。

 

不考慮IE7的話,數組項合並是最慢的連接字符串方法之一。推薦使用++=操作符代替。

 

回溯既是正則表達式匹配功能的基本組成部分,也是正則的低效原因。

 

回溯失控發生在正則本應快速匹配的地方,但因某些特殊的字符串匹配動作導致運行緩慢甚至瀏覽器崩潰。避免這個問題的方法是:使相鄰的資源互斥,避免嵌套量詞對同一字符串的相同部分多次匹配,通過重復利用預查的原子組去除不必要的回溯。

 

正則表達式並不總是完成工作的最佳工具,尤其當你只搜索字面字符串的時候。

 

去除字符串收尾空白有很多方法,但是用兩個簡單的正則表達式(一個去除頭部一個去除尾部)來處理大量字符串內容能提供一個簡潔而跨瀏覽器的方法。

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM