1. 程式人生 > >高效能JavaScript(字串和正則表示式)

高效能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的話,陣列項合併是最慢的連線字串方法之一。推薦使用++=操作符代替。

 

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

 

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

 

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

 

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