JavaScript正則表達(dá)式這幾個(gè)細(xì)節(jié)你真的知道?
粗淺的編寫(xiě)正則表達(dá)式,是造成性能瓶頸的主要原因。如下:
var reg1 = /(A+A+)+B/; var reg2 = /AA+B/;
上述兩個(gè)正則表達(dá)式,匹配效果是一樣的,但是,效率就相差太遠(yuǎn)了,甚至在與少量字符串匹配時(shí),reg1就會(huì)造成你瀏覽器卡死。
不信?我們可以測(cè)試下。
首先,我們聲明一個(gè)字符串變量str,同時(shí)賦予一個(gè)包含20個(gè)A的字符串給str,采用match方法與上述reg1、reg2進(jìn)行匹配測(cè)試,如下:
var str = ’AAAAAAAAAAAAAAAAAAAA’;str.match(reg1);str.match(reg2);
在瀏覽器中運(yùn)行該段代碼,發(fā)現(xiàn)一切正常嘛。
然而,隨著,我們不斷向變量str中添加A后,重復(fù)測(cè)試,在某一刻(取決于你的瀏覽器),reg1就會(huì)讓我們的瀏覽器掛起,但,回頭看看最終的str字符串長(zhǎng)度,卻還不到50。而,reg2卻安然無(wú)恙。
心里有一絲疑問(wèn),是什么造成了它們?nèi)绱司薮蟮牟顒e?以后我們?cè)趯?xiě)正則表達(dá)式時(shí),又該如何避免防范這類問(wèn)題呢?
那么,接下來(lái),我們就有必要深入理解JavaScript正則表達(dá)式的內(nèi)部執(zhí)行原理了。
如果,在此你還不是很了解正則表達(dá)式,那么可以參考如下兩篇博客后,再前來(lái),小生在此等候。
正則表達(dá)式工作原理為了高效的使用正則表達(dá)式,理解它們的工作原理是很重要的。
具體如下:
Step1.編譯
當(dāng)我們創(chuàng)建一個(gè)正則表達(dá)式(字面量或者RegExp對(duì)象)后,瀏覽器會(huì)檢查該正則的模板是否符合標(biāo)準(zhǔn),然后將其轉(zhuǎn)化成內(nèi)部代碼,用于執(zhí)行匹配工作。
所以,如果我們將正則表達(dá)式賦予一個(gè)變量,可以避免重復(fù)執(zhí)行該 ‘編譯’ 步驟。
Step2.設(shè)置開(kāi)始位置
當(dāng)我們使用Step1中編譯后的正則表達(dá)式時(shí),首先它將確定從目標(biāo)字符串中什么位置進(jìn)行匹配。通常,是目標(biāo)字符串的起始位置,或者由正則表達(dá)式的lastIndex屬性指定。
但是,當(dāng)它從Step4(匹配失?。┲蟹祷貢r(shí),該位置則為匹配失敗的位置的下一個(gè)位置。
Step3.正則匹配
當(dāng)經(jīng)歷Step2后,正則表達(dá)式將從指定位置,從左到右,與目標(biāo)字符串,逐個(gè)匹配。若,正則表達(dá)式在匹配過(guò)程中,遇到某個(gè)字元匹配不了時(shí),它不會(huì)立即失敗,而是嘗試回溯到最近一個(gè)決策點(diǎn),然后在剩余選項(xiàng)中選擇一個(gè),以求繼續(xù)能匹配。
Step4.匹配結(jié)果
當(dāng)經(jīng)歷Step3后,發(fā)現(xiàn)能與正則匹配成功的子字符串,那么就匹配成功。如果,經(jīng)歷了Step3后,發(fā)現(xiàn)沒(méi)有能與正則匹配的子字符串,那么,它將回到Step2,繼續(xù)。只有當(dāng)目標(biāo)字符串中的每個(gè)字符(以及最后一個(gè)字符后面的位置)都經(jīng)歷了Step3后,仍沒(méi)有找到匹配項(xiàng),才宣布失敗。
下面就舉個(gè)例子,使我們更透徹地明白以上4步。
如下:
var reg = /A(B|C)D/g; var str = ’ABCACD’;reg.exec(str);
① 首先,瀏覽器將解析reg正則表達(dá)式(Step1)。
② 然后,由于是首次匹配,所以確認(rèn)開(kāi)始位置即為字符串起始位置(Step2)。
③ 首先由正則的第一個(gè)字元A與字符串起始位置字符A匹配,成功,并在之后的位置記錄一個(gè)決策點(diǎn),因?yàn)楹竺嬗蟹种铮蝗缓笥?(B|C)分支中的B選項(xiàng)去匹配字符串的B,發(fā)現(xiàn)匹配;然后再由正則下一個(gè)字元D去匹配目標(biāo)字符串第三個(gè)字符C,發(fā)現(xiàn)不匹配,但是并沒(méi)有放棄,而是回溯,查看是否有決策點(diǎn),發(fā)現(xiàn)有,回溯到就近一個(gè)決策點(diǎn)(字符串首字母A之后的那個(gè)位置上),嘗試?yán)玫诙€(gè)分支選項(xiàng)C去匹配字符串第二個(gè)字符B,發(fā)現(xiàn)不匹配,回溯,查詢是否還有其他分支選項(xiàng),發(fā)現(xiàn)沒(méi)有,然后宣布該次失敗(Step3)。
④ 經(jīng)歷Step3后,發(fā)現(xiàn)沒(méi)有與正則匹配的子字符串,但是,與之匹配的目標(biāo)字符串的匹配位置并不是最后一個(gè)位置,所以,回到Step2,從目標(biāo)字符串的下一個(gè)位置(即,字符串首字母A之后的那個(gè)位置上)開(kāi)始匹配。首先由正則表達(dá)式的第一個(gè)字元A與目標(biāo)字符串B匹配,不成功,又無(wú)回溯點(diǎn),故而,進(jìn)入Step4,判斷是否是最后一個(gè)位置,發(fā)現(xiàn)不是,又跳到Step2中繼續(xù)。
⑤ 就這樣一步一步,來(lái)到了目標(biāo)字符串的第四個(gè)位置,首先A去與目標(biāo)字符串的第三個(gè)字符A匹配,成功;接下來(lái)就是由分支(B|C),去匹配C,首先由分支中的第一個(gè)選項(xiàng)B去與C匹配,發(fā)現(xiàn)沒(méi)有成功,回溯到就近一個(gè)決策點(diǎn),嘗試?yán)玫诙€(gè)分支選項(xiàng)C匹配,成功,緊接著D也成功了。
⑥ 匹配成功,并將lastIndex置為6。
上述 “正則表達(dá)式工作原理” 一小節(jié),Step3中的回溯我們是一筆帶過(guò)的。但是,可不要忽略了,回溯在正則中是非常重要的,如果理解得不明白,我們?cè)诰帉?xiě)正則時(shí),很容易造成回溯失控。
下面我們就來(lái)一起看看回溯在正則表達(dá)式中的運(yùn)用。
正則表達(dá)式中有兩種情況,會(huì)制造回溯點(diǎn):
-分支(通過(guò)|操作符)
-量詞(諸如*,+?,或者{…})
下面我們就分別舉例來(lái)看看。
分支和回溯
對(duì)于分支,詳見(jiàn) ‘正則表達(dá)式工作原理’ 小節(jié)中Demo。
量詞和回溯
在量詞中,有貪婪量詞(諸如*,+)和非貪婪量詞(諸如*?,+?)之分。所以回溯形式對(duì)于它們而言也就有差別咯。我們首先寫(xiě)個(gè)demo看看貪婪量詞是怎么回溯的。
Demo如下:
var reg = /w*D/g; var str = ’MonkyDorie!’;reg.exec(str);
就上述貪婪模式匹配流程如下:
提醒:正則表達(dá)式reg中w表示匹配“字母、數(shù)字或下劃線”,*這個(gè)貪婪量詞表示重復(fù)匹配零次或者多次,由于是貪婪量詞,故而它會(huì)盡可能多的匹配。
① 首先,正則中的w*與目標(biāo)字符串匹配,會(huì)一直匹配到‘!’之前,即’MonkyDorie’,并且,每個(gè)匹配位置都會(huì)記錄一個(gè)決策點(diǎn),便于回溯。
② 然后,由正則中的剩余字元D與字符串中!匹配,匹配失??;但是,它并沒(méi)有放棄(因?yàn)樵诖酥埃涗浟藳Q策點(diǎn)),而是回溯到就近一個(gè)決策點(diǎn)(字符e的前一個(gè)位置),然后正則D與字符e匹配,匹配失??;再回溯到就近一個(gè)決策點(diǎn)(字符i的前一個(gè)位置),然后正則D與字符i匹配,匹配失敗;就這樣一直回溯到字符D的前一個(gè)位置時(shí),正則D與字符D匹配,匹配成功,并置lastIndex為6。
好了,這就是上述貪婪匹配流程。
隨后,我們將上述Demo中的正則表達(dá)式,稍微調(diào)整下,在*后面加上?,變成非貪婪模式,看看非貪婪量詞是怎么回溯的。
Demo如下:
var reg = /w*?D/g; var str = ’MonkyDorie!’;reg.exec(str);
就上述非貪婪模式匹配流程如下:
提醒:正則表達(dá)式reg中w表示匹配“字母、數(shù)字或下劃線”,*?是個(gè)非貪婪量詞,也表示重復(fù)匹配零次或者多次,但是由于是非貪婪量詞,故而它會(huì)盡可能少的匹配。
首先,正則中的w*?會(huì)選擇匹配零個(gè)字符(盡可能少的匹配),并將第一個(gè)位置(字符M的前一個(gè)位置)記錄一個(gè)決策點(diǎn),繼而輪到字元D與字符串字符M匹配,匹配失??;回溯到就近一個(gè)決策點(diǎn)(字符M的前一個(gè)位置),然后w*?選擇匹配一個(gè)字符M,并記錄一個(gè)回溯點(diǎn)(第二個(gè)字符o的前一個(gè)位置),繼而輪到字元D與字符串字符o匹配,匹配失??;回溯到就近一個(gè)決策點(diǎn)(字符o的前一個(gè)位置),就這樣一步一步,當(dāng)w*?選擇匹配五個(gè)字符Monky時(shí),繼而輪到字元D與字符串字符D匹配,匹配成功,并置lastIndex為6.
上述兩Demo,對(duì)比圖如下:
正如上述 ‘回溯’ 小節(jié)中談到,重復(fù)量詞和分支會(huì)記錄決策點(diǎn),引起回溯。但是,如果在實(shí)際需求中,我們不想讓它們記錄決策點(diǎn)呢—因?yàn)榛厮萏嗑蜁?huì)導(dǎo)致回溯失控,影響性能,正如我們?cè)?‘前言’ 中看到的那樣。
一些正則表達(dá)式引擎,支持一種叫做原子組的屬性。原子組,寫(xiě)作(?>…),省略號(hào)表示任意正則表達(dá)式模板。存在原子組中的正則表達(dá)式組中的任何決策點(diǎn)都將被丟棄。利用原子組,我們就可以在必要時(shí),消除由重復(fù)量詞和分支記錄的決策點(diǎn)了。
但,在JavaScript中不支持原子組,怎么辦呢?
我們可以利用前瞻來(lái)模擬原子組,但是,前瞻在整個(gè)匹配過(guò)程中,是不消耗字符的,它只是檢查自己包含的模板是否能在當(dāng)前位置匹配。然而,我們又可以利用后向引用解決此問(wèn)題,如下:
(?=(pattern to make atomic))1
好了,針對(duì) ‘利用前瞻和后向引用避免回溯’ 一節(jié),我們寫(xiě)個(gè)Demo,自我測(cè)試下:
var str = ’ABCDM’; //目標(biāo)字符串 var reg1 = /w*M/; //貪婪模式 var reg2 = /(?=(w*))1M/; //貪婪模式,使用前瞻和后向引用 var reg3 = /w*?M/; //非貪婪模式 var reg4 = /(?=(w*?))M/; //非貪婪模式,使用前瞻和后向引用
對(duì)于以下匹配結(jié)果,各位看官答對(duì)否:
str.match(reg1);str.match(reg2);str.match(reg3);str.match(reg4);
來(lái)自:http://www.cnblogs.com/giggle/p/5990486.html
相關(guān)文章:
1. Python如何批量生成和調(diào)用變量2. 基于 Python 實(shí)踐感知器分類算法3. ASP.Net Core對(duì)USB攝像頭進(jìn)行截圖4. Python 中如何使用 virtualenv 管理虛擬環(huán)境5. python利用opencv實(shí)現(xiàn)顏色檢測(cè)6. ASP.NET MVC實(shí)現(xiàn)橫向展示購(gòu)物車(chē)7. 通過(guò)CSS數(shù)學(xué)函數(shù)實(shí)現(xiàn)動(dòng)畫(huà)特效8. ASP.Net Core(C#)創(chuàng)建Web站點(diǎn)的實(shí)現(xiàn)9. ajax動(dòng)態(tài)加載json數(shù)據(jù)并詳細(xì)解析10. windows服務(wù)器使用IIS時(shí)thinkphp搜索中文無(wú)效問(wèn)題
