引言:JS 正則表達式是 JS 學(xué)習(xí)過程中的一大難點,繁雜的匹配模式足以讓人頭大,不過其復(fù)雜性和其學(xué)習(xí)難度也賦予了它強大的功能。文章從 JS 正則表達式的正向前瞻說起,實現(xiàn)否定匹配的案例。本文適合有一定 JS 正則表達式基礎(chǔ)的同學(xué),如果對正則表達式并不了解,還需先學(xué)習(xí)基礎(chǔ)再來觀摩這門否定大法。


 一、標(biāo)簽過濾需求

  不知道大家在寫JS有沒有遇到過這樣的情況,當(dāng)你要處理一串字符串時,需要寫一個正則表達式來匹配當(dāng)中不是 XXX 的文本內(nèi)容。聽起來好像略有些奇怪,匹配不是 XXX 的內(nèi)容,不是 XXX 我匹配它干嘛啊,我要啥匹配啥不就完了。你還別說,這個玩意還真的有用,不管你遇沒遇到過,反正我是遇到了。具體的需求例如:當(dāng)你收到一串HTML代碼,需要對這一串HTML代碼過濾,將里面所有的非<p>標(biāo)簽都改為<p>。這里肯定有不少同學(xué)就要嫌棄了,“將所有標(biāo)簽都改為<p>,那就把任意標(biāo)簽都改為<p>不就完了?”,于是乎一行代碼拍腦袋而生:

1     var str = '<div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>';2     var reg = /<(\/?).*?>/g;3     var newStr = str.replace(reg, "<$1p>");4     console.log(newStr);//<p>,<p>,<p>,<p>,</p>,</p>,</p>,</p>

  注意這個方法中有一個引用符 “$1” ,這個的意思引用正則的表達式的第1個分組,可以用$N來表示在正則表達式中的第N個捕獲的引用。就那上面的例子來說,"(\/?)"這個一個表達式的含義是,"\/"這個字符出現(xiàn)0次或者1次,而$1這個引用呢就相當(dāng)于和“\/”這個字符門當(dāng)戶對的大閨女,她已下定決心此生非"\/"不嫁。所以當(dāng)匹配到有一個“\/”的時候,$1這個引用就把它捕獲下來,從現(xiàn)在起,你的就是我的,我的就是你的啦,因此$1等價于"(\/?)"所匹配到的字符;反之如果沒有匹配到"\/"這個字符,那$1這個引用就得空守閨房,獨立熬過一個又一個漫長的夜晚,因為它內(nèi)心極度的空虛,所以$1就等價于""(也就是空串)。

  這里先聊了聊引用和捕獲的概念,因為后面還會用到它。那么話說回來,剛才那一串正則,不是已經(jīng)完美的實現(xiàn)了需求了嗎?還研究什么否定匹配???各位看官別急,且聽小生慢慢道來。我們都知道,需求這個東西,肯定是會改嘀(???)?,F(xiàn)在改一改需求:當(dāng)你收到一串HTML代碼,需要對這一串HTML代碼過濾,將里面所有的非<p>或者<div>標(biāo)簽都改為<p>。WTF?這算哪門子需求?話說我當(dāng)時也是這種反應(yīng)。我們現(xiàn)在分析一下這個需求到底要干嘛,也就是說,保留原HTML代碼中的<p>和<div>,將其他標(biāo)簽統(tǒng)一修改為<p>。咦...這下可不好弄了,剛才那串代碼看上去貌似行不通了。所以說這時候就只能用排除法了,排除掉<p>和<div>,替換掉其他的標(biāo)簽。那么問題也就來了,如何排除?

 二、正則前瞻表達式

  在正則表達式當(dāng)中有個東西叫做前瞻,有的管它叫零寬斷言:

表達式名稱描述
(?=exp)正向前瞻匹配后面滿足表達式exp的位置
(?!exp)負向前瞻匹配后面不滿足表達式exp的位置
(?<=exp)正向后瞻匹配前面滿足表達式exp的位置(JS不支持
(?<!exp)負向后瞻匹配前面不滿足表達式exp的位置(JS不支持

  由于 JS 原生不支持后瞻,所以這里就不研究它了。我們來看看前瞻的作用:

1     var str = 'Hello, Hi, I am Hilary.';2     var reg = /H(?=i)/g;3     var newStr = str.replace(reg, "T");4     console.log(newStr);//Hello, Ti, I am Tilary.

  在這個DEMO中我們可以看出正向前瞻的作用,同樣是字符"H",但是只匹配"H"后面緊跟"i"的"H"。就相當(dāng)于有一家公司reg,這時候有多名"H"人員前來應(yīng)聘,但是reg公司提出了一個硬條件是必須掌握"i"這項技能,所以"Hello"就自然的被淘汰掉了。

  那么負向前瞻呢?道理是相同的:

1     var str = 'Hello, Hi, I am Hilary.';2     var reg = /H(?!i)/g;3     var newStr = str.replace(reg, "T");4     console.log(newStr);//Tello, Hi, I am Hilary.

  在這個DEMO中,我們把之前的正向前瞻換成了負向前瞻。這個正則的意思就是,匹配"H",且后面不能跟著一個"i"。這時候"Hello"就可以成功的應(yīng)聘了,因為reg公司修改了他們的招聘條件,他們說"i"這門技術(shù)會有損公司的企業(yè)文化,所以我們不要了。

 三、前瞻的非捕獲性

  說到這里,讓我們回到最初的那個需求,讓我們先用負向前瞻來實現(xiàn)第一個需求:將所有非<p>標(biāo)簽替換為<p>。話說同學(xué)們剛學(xué)完了負向前瞻,了解到了JS的博大精深,心中暗生竊喜,提筆一揮:

1     var str = '<div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>';2     var reg = /<(\/?)(?!p)>/g;3     var newStr = str.replace(reg, "<$1p>");4     console.log(newStr);//<div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>

  What?為什么不起作用呢?說好的否定大法呢?這里就得聊一聊前瞻的一個特性,前瞻是非捕獲性分組,什么玩意是非捕獲性分組呢?還記得前面那位非"\/"不嫁的大閨女$1嗎,人家為什么那么一往情深,是因為她早已將"\/"的心捕獲了起來,而前瞻卻是非捕獲性分組,也就是你捕獲不到人家。也就是說無法通過引用符"\n"或者"$n"來對其引用:

1     var str = 'Hello, Hi, I am Hilary.';2     var reg = /H(?!i)/g;3     var newStr = str.replace(reg, "T$1");4     console.log(newStr);//T$1ello, Hi, I am Hilary.

  注意其中輸出的語句,前面我們可以看到,如果引用符沒有匹配到指定的字符,那么就會顯示空串"",可是這里是直接顯示了整個引用符"$1"。這是因為前瞻表達式根本就沒有捕獲,沒有捕獲也就沒有引用。

  非捕獲性是前瞻的一個基本特征,前瞻的另外一個特性是不吃字符,意思就是前瞻的作用只是為了匹配滿足前瞻表達式的字符,而不匹配前瞻本身。也就是說前瞻不會修改匹配位置,這么說我自己都覺得晦澀,我們還是來看看代碼吧︽⊙_⊙︽:

1     var str = 'Hello, Hi, I am Handsome Hilary.';2     var reg = /H(?!i)e/g;3     var newStr = str.replace(reg, "T");4     console.log(newStr);//Tllo, Hi, I am Handsome Hilary.

  注意觀察輸出的字符串,前瞻的作用僅僅是匹配出滿足前瞻條件的字符"H",匹配出了"Hello"和"Handsome"當(dāng)中的H,但同時前瞻不會吃字符,也就是不會改變位置,接下來還是會緊接著"H"開始繼續(xù)往下匹配,這時候匹配條件是"e",于是"Hello"中的"He"就匹配成功了,而"Handsome"中的"Ha"則匹配失敗。

1. /H(?!i)/g --> Hello, Hi, I am Handsome Hilary.
2. /H(?!i)e/g --> Hello, Hi, I am Handsome Hilary.

 四、用前瞻實現(xiàn)標(biāo)簽過濾

  既然前瞻是非捕獲性的,而且還不吃字符,那么了解到這些特征后我們現(xiàn)在終于可以完成我們的需求了吧?因為它不吃字符,所以具體的標(biāo)簽字符還得由我們自己來吃:

1     var str = '<div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>';2     var reg = /<(\/?)(?!p|\/p).*?>/g;3     var newStr = str.replace(reg, "<$1p>");4     console.log(newStr);//<p>,<p>,<p>,<p>,</p>,</p>,</p>,</p>

  聊了這么半天,終于解決了咱們的第一個需求,注意當(dāng)中的".*?",雖然這里匹配的是任意字符,但是別忘了,有了前面的負向前瞻,我們匹配到的都是后面不會緊跟著"p"或者"/p"的字符"<"。

/<(?!p|\/p)/g --> <div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>

  注意在這里用了一個管道符"|"來匹配"\/p",雖然前面已經(jīng)有了"(\/?)"匹配結(jié)束符,但是切記這里的分組選項不能省略,因為這里的量詞是可以出現(xiàn)0次。我們來試想一下如果用"/<(\/?)(?!p).*?>/g"來匹配"</p>"這個標(biāo)簽,當(dāng)量次匹配到"/"的時候,發(fā)現(xiàn)可以匹配,便記錄下來,然后對"/"進行前瞻判斷,但是后面卻接著一個"p"于是不能匹配,丟掉;注意這時"(\/?)"的匹配字符是0個,于是乎轉(zhuǎn)而對"<"進行前瞻判斷,這里的"<"后面緊接著的是"/p"而不是"p",于是乎成功匹配,所以這個標(biāo)簽會被替換掉;而且,由于之前的分組匹配到的字符是0個,也就是沒有匹配到字符,所以后面的引用是個空串。

1     var str = '<div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>';2     var reg = /<(\/?)(?!p).*?>/g;3     var newStr = str.replace(reg, "<$1p>");4     console.log(newStr);//<p>,<p>,<p>,<p>,</p>,</p>,<p>,</p>

  完成了第一個過濾需求,那么第二個過濾需求也就自然而然的完成了,這時候,就算有那么五六個標(biāo)簽需要保留,咱們也不用怕了:

1     var str = '<div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>';2     var reg = /<(\/?)(?!p|\/p|div|\/div).*?>/g;3     var newStr = str.replace(reg, "<$1p>");4     console.log(newStr);//<div>,<p>,<p>,<p>,</p>,</p>,</p>,</div>

 五、總結(jié)

  JS 的正向前瞻只是正則表達式當(dāng)中一部分,沒相當(dāng)就這么一部分還有著這么多的奧妙呢。

  在使用正向前瞻,我們需要注意的是:

  •   前瞻是非捕獲性的:其特征是無法引用。

  •   前瞻不消耗字符:前瞻只匹配滿足前瞻表達式的字符,而不匹配其本身。

  話說,咱們的需求就到這了嗎?真的就完了嗎?同學(xué)們覺得過癮不?有些同學(xué)覺得可能差不多了,需要消化一段時間,但是絕對有那么一部分同學(xué)還完全沒過癮呢,沒關(guān)系,最后留給大家一道思考題,截止到我寫這篇博客為止,我還沒有想出一個解決辦法呢(? ?_?)?。

  需求如下:當(dāng)你收到一串HTML代碼,需要對這一串HTML代碼過濾,將里面所有的非<p>或者<div>標(biāo)簽都改為<p>,并且保留所有標(biāo)簽的樣式,要求只使用一個正則表達式,例如:

平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),游戲開發(fā),動畫培訓(xùn)

//輸入var input = '<div class="beautiful">,<p class="provocative">,<h1 class="attractive" id="header">,<span class="sexy">,</span>,</h1>,</p>,</div>';//輸出var output = '<div class="beautiful">,<p class="provocative">,<p class="attractive" id="header">,<p class="sexy">,</p>,</p>,</p>,</div>';

平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),游戲開發(fā),動畫培訓(xùn)

  如果你有好的解決方案,歡迎在評論區(qū)留言,大家一起學(xué)習(xí)。

  

  參考文獻:

  devinran —— 《相愛相殺——正則與瀏覽器的愛恨情仇》

http://www.cnblogs.com/dong-xu/p/6926064.html