讀古今文學網 > 編寫高質量代碼:改善JavaScript程序的188個建議 > 建議48:慎用正則表達式修剪字符串 >

建議48:慎用正則表達式修剪字符串

(1)使用兩個子表達式修剪字符串

去除字符串首尾的空格是一個簡單而常見的任務,但到目前為止JavaScript還沒有實現它。正則表達式允許用很少的代碼實現一個修剪函數,最好的全面解決方案可能是使用兩個子表達式:一個用於去除頭部空格,另一個用於去除尾部空格。這樣處理簡單而快速,特別是處理長字符串時。


if(!String.prototype.trim){

String.prototype.trim=function{

return this.replace(/^s+/,\"\").replace(/s+$/,\"\");

}

}

var str=\"tn test string\".trim;

alert(str==\"test string\");//alerts\"true\"


使用if語句進行檢測,如果已經存在trim原生函數,則不要覆蓋trim原生函數,因為原生函數進行了優化後通常遠遠快於自定義函數。使用上面代碼在Firefox瀏覽器中大約有35%的性能提升(或多或少依賴於目標字符串的長度和內容)。將/s+$/(第二個正則表達式)替換成/ss*$/。雖然這兩個正則表達式的功能完全相同,但是Firefox瀏覽器卻為那些以非量詞字元開頭的正則表達式提供額外的優化。在其他瀏覽器上,差異不顯著,或者優化完全不同。

然而,改變正則表達式,在字符串開頭匹配/^ss*/不會產生明顯差異,因為^錨需要「照顧」那些快速作廢的非匹配位置(避免一個輕微的性能差異,因為在一個長字符串中可能產生上千次匹配嘗試)。

(2)使用一個正則表達式修剪字符串

事實上,除這裡列出的方法外還有許多其他方法,可以寫一個正則表達式來修剪字符串,但在處理長字符串時,這種方法執行速度總比用兩個簡單的表達式要慢。


String.prototype.trim=function{

return this.replace(/^s+|s+$/g,\"\");

}


這可能是最通常的解決方案。它通過分支功能合併了兩個簡單的正則表達式,並使用/g(全局)標記替換所有匹配,而不只是第一個匹配(當目標字符串首尾都有空格時將匹配兩次)。這並不是一個「可怕」的方法,但在對長字符串操作時,它比使用兩個簡單的子表達式要慢,因為兩個分支選項都要測試每個字符位置。


String.prototype.trim=function{

return this.replace(/^s*([sS]*?)s*$/,\"$1\");

}


這個正則表達式的工作原理是匹配整個字符串,捕獲從第一個到最後一個非空格字符之間的序列,記入後向引用1。然後使用後向引用1替代整個字符串,就留下了這個字符串的修剪版本。

這個方法概念簡單,但捕獲組中的「懶惰」量詞使正則表達式進行了許多額外操作(如回溯),因此在操作長目標字符串時很慢。在進入正則表達式捕獲組時,[sS]類的「懶惰」量詞*?要求捕獲組盡可能地減少重複次數。因此,這個正則表達式每匹配一個字符,都要停下來嘗試匹配餘下的s*$模板。如果由於字符串當前位置之後存在非空格字符而導致匹配失敗,正則表達式將匹配一個或多個字符,更新後向引用,然後再次嘗試匹配模板的剩餘部分。


String.prototype.trim=function{

return this.replace(/^s*([sS]*S)?s*$/,\"$1\");

}


這個表達式與上一個很像,但出於性能原因以「貪婪」量詞取代了「懶惰」量詞。為確保捕獲組只匹配到最後一個非空格字符,必須尾隨一個S。然而,由於正則表達式必須匹配全部由空格組成的字符串,整個捕獲組通過尾隨一個?量詞而成為可選組。

在此,[sS]*中的「貪婪」量詞「*」表示重複方括號中的任意字符模板直至字符串結束。然後,正則表達式每次回溯一個字符,直到它能夠匹配後面的S,或者直到回溯到第一個字符而匹配整個組(之後它跳過這個組)。

如果尾部空格不比其他字符串更多,通過一個表達修剪的方案通常比前面那些使用「懶惰」量詞的方案更快。事實上,這個方案在IE、Safari、Chrome和Opera瀏覽器上執行速度如此之快,甚至超過使用兩個子表達式的方案,是因為這些瀏覽器包含特殊優化,專門服務於為字符類匹配任意字符的「貪婪」重複操作,正則表達式引擎直接跳到字符串末尾而不檢查中間的字符(儘管回溯點必須被記下來),然後適當回溯。不幸的是,這種方法在Firefox和Opera 9瀏覽器上執行得非常慢,所以到目前為止,使用兩個子表達式仍然是更好的跨瀏覽器方案。


String.prototype.trim=function{

return this.replace(/^s*(S*(s+S+)*)s*$/,\"$1\");

}


這是一個相當普遍的方法,但沒有很好的理由使用它,因為它在所有瀏覽器上都是這裡所列出的所有方法中執行得最慢的一個。這類似於最後兩個正則表達式,它匹配整個字符串然後用打算保留的部分替換這個字符串,因為內部組每次只匹配一個單詞,正則表達式必須執行大量的離散步驟。修剪短字符串時性能衝擊並不明顯,但處理包含多個詞的長字符串時,這個正則表達式可以成為影響性能的一個問題。

將內部組修改為一個非捕獲組,例如,將(s+S+)修改為(?:s+S+),在Opera、IE和Chrome瀏覽器上縮減了大約20%~45%的處理時間,在Safari和Firefox瀏覽器上也有輕微改善。儘管如此,一個非捕獲組不能完全代換這個實現。注意,外部組不能轉換為非捕獲組,因為它在被替換的字符串中被引用了。

雖然正則表達式的執行速度很快,但是沒有它們幫助時修剪字符串的性能還是值得考慮的。例如:


String.prototype.trim=function{

var start=0,

end=this.length-1,

ws=\"nrtfx0bxa0u1680u180eu2000u2001u2002u2003

u2004u2005u2006u2007u2008u2009u200au200bu2028u2029u202fu205fu3000ufeff\";

while(ws.indexOf(this.charAt(start))>-1){

start++;

}

while(end>start&&ws.indexOf(this.charAt(end))>-1){

end--;

}

return this.slice(start,end+1);

}


在上面代碼中,ws變量包括在ECMAScript v5中定義的所有空白字符。出於效率方面的考慮,在得到修剪區的起始和終止位置之前避免複製字符串的任何部分。

當字符串末尾只有少量空格時,這種情況使正則表達式處於無序狀態。原因是,儘管正則表達式很好地去除了字符串頭部的空格,卻不能同樣快速地修剪長字符串的尾部。一個正則表達式不能跳到字符串的末尾而不考慮沿途字符。正因如此,在第二個while循環中從字符串末尾向前查找一個非空格字符。

雖然上面代碼不受字符串總長度影響,但是它有自己的弱點——長的頭尾空格,因為循環檢查字符是不是空格在效率上不如正則表達式所使用的優化過的搜索代碼。

(3)正則表達式與非正則表達式結合起來修剪字符串

最後一個辦法是將正則表達式與非正則表達式兩者結合起來,用正則表達式修剪頭部空格,用非正則表達式方法修剪尾部字符。


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

}


當只修剪一個空格時,此混合方法非常快,同時去除了性能上的風險,如以長空格開頭的字符串,完全由空格組成的字符串(儘管它在處理尾部長空格的字符串時仍具有弱點)。

注意:此方案在循環中使用正則表達式檢測字符串尾部的字符是否為空格,雖然使用正則表達式增加了一點性能負擔,但是它允許根據瀏覽器定義空格字符列表,以保持簡短和兼容性。

所有修剪方法總的趨勢:在基於正則表達式的方案中,字符串總長比修剪掉的字符數量更影響性能;而非正則表達式方案從字符串末尾反向查找,不受字符串總長的影響,但明顯受到修剪空格數量的影響。簡單地使用兩個子正則表達式在所有瀏覽器上處理不同內容和長度的字符串時,均表現出穩定的性能,因此可以說這種方案是最全面的解決方案。混合解決方案在處理長字符串時特別快,其代價是代碼稍長,在某些瀏覽器上處理尾部長空格時存在弱點。