一道小小的題目引發對javascript支援正規表示式相關方法的探討

2ue發表於2017-11-24

本文釋出在我的部落格一道小小的題目引發對javascript支援正規表示式相關方法的探討
許可協議: 署名-非商業性使用-禁止演繹 4.0 國際 轉載請保留原文連結及作者。


以前對於正則是非常懼怕的,因為看不懂和學不會。但最近專案中頻繁的使用到了正則,因此強迫自己去學習瞭解,慢慢的體會到了他的魅力與強大。當然學習正則初入門的時候有些枯燥難懂,但越學越覺得輕鬆。本文不準備說關於正則本身的事兒,而是說一說關於javascript中關於正則的幾個方法中被很多人忽略的地方。

工具

說到正則,很多人都是從抄到改到自己寫,這個過程可能有時候很漫長。如一些工具能幫助你快速分析和學習正則,那麼學習的過程你肯定要輕鬆得多。下面我推薦兩個我經常使用的正則線上視覺化工具,正則視覺化工具圖解符合鐵路圖規律(其實不明白什麼是鐵路一樣很容易看懂,只是一些細微的地方和我們的常規思維有點差別)。

  • regexper 我最常用的一個,個人覺得UI做得比其他好
  • regulex 備選,他有一個很舒心的功能,可以提供一段js,巢狀到你的網站,生成正則視覺化圖

一道小小的題目

這道題目是在群裡日常閒聊時,公司同事丟擲來的,具體是出自哪裡本人沒去考察。先先說說題目:

寫一個方法把一個數字末尾的連續0變成9,如1230000變成1239999

一道很簡單的題目,直接正則就能搞定,也許你會寫:

function zoreToNine(num){
    return (num + '').replace(/0/g,9);
}
//或者
function zoreToNine(num){
    return (num + '').replace(/[1-9]0+$/,9);
}複製程式碼

這也是此題的陷阱所在,按照上面的方法,1023000就會被轉化成1923999,這樣是不符合要求的,所以改進一下:

function zoreToNine(num){
    return (num + '').replace(/[1-9]0+$/,function($1){
        return $1.replace(/0/g,9);
    });
}
zoreToNine(1223000); //1223999
zoreToNine(1023000); //1023999複製程式碼

關於這個問題的解決方案@微醺歲月同學提供了一種,位置匹配的方法,簡單了很多,厲害!

"12300100000".replace(/0(?=(0+$)|\b)/g,9); //12300199999複製程式碼

當然解決問題的方法很多,不一定非要用正則,還完全可以使用純算術的方法實現,大家有興趣可以嘗試,閒話少說進入這次的主題:javascript支援正規表示式相關方法,注意並不是正則物件的方法。
上述方法使用了正則,有趣的是在回撥函式裡有一個$1,這個$1到底是什麼?所有的匹配規則匹配後都有$1這個變數麼?...一連串的問題,以前我從來沒有去追探過,趁著昨個比較空閒,去追探了一番,並在今天整理了一下,寫下此文記錄。

主角

javascript中正則物件有三個方法:testexeccompile,但是此次的主角並不是它們!我們討論的是能夠使用正則表示的相關方法:searchmatchreplacesplit,注意它們都是String物件的方法,使用它們必須要是String型別.

replace(rule[regexp/substr], replacement)

replace是一個用於替換字串的方法,雖然看似簡單,但是它隱藏的機關也是常常被人忽略。具體分析一下它的特點:
它接收兩個引數
無副作用不影響原始變數
返回被改變的字串(一定是字串型別)

定義一些變數,方便全文取用。

let a = '12309800', b = '12309800[object Object]', b = '12309800{}';複製程式碼

引數rule

在一般情況,rule引數一般是正則、字串、數字。
如果是字串,將會在匹配到第一個符合條件的目標,結束方法;
如果是正則,則按照正則的規則進行匹配

//匹配第一個0替換成5
a.replace(0,5); //'12359800'
//匹配所有的0替換成5
a.replace(/0/g,5); //'12359855'複製程式碼

引數replacement

在一般情況,replacement引數是字串、數字、者回撥。

包含$的字串

當引數rule為正則,並且正則至少包含有一對完整的()時,如果replacement包含有$的字串,那麼對於$n(n為大於0的整數,n的長度取決於正則中括號的對數),會被解析成一個變數。但是也僅僅只是作為一個變數,無法在字串中進行計算,此時更類似特別的字串模板變數。

一般情況下,$n中n的長度取決於正則中括號的對數,$1表示第1對括號匹配的結果,$2表示第2對匹配的結果...在正則所有的括號對中,左括號出現在第幾個位置(或者說從左往右),則它就是第幾對括號,以此類推。姑且我們把這種規則成為正則匹配分割規則(ps:這完全是我自己取的一個名字,方便文章後面使用和記憶)。

a.replace(0,'$0'); //'123$09800'
a.replace(/00/g,'$0'); //'123098$0'
a.replace(/[1-9]0+$/,'$1'); //'12309$1'
a.replace(/([1-9](0+$))/,'$1'); //'12309800',此時$1為[1-9](0+$)匹配到的內容,$2為0+$匹配到的內容
a.replace(/([1-9])(0+$)/,'$1'); //'123098',此時$1為[1-9]匹配到的內容,$2為0+$匹配到的內容
a.replace(/([1-9])(0+$)/,'$1*$2'); //'123098*00',此處的$1和$2不會安照期待的情況進行乘法計算,要進行計算可以用回撥複製程式碼

請注意:雖然目前引數replacement中攜帶有$n仍然能正常使用,但是這種方式已經不被規範所推薦,更應該使用回撥來完成這個操作。這一點謝謝@lucky4同學的指出

如果正則中包含有全域性匹配標誌(g),那麼每次匹配的都符合上述規則

回撥函式

先看例子:

a.replace(/[1-9]0+$/,function(){
    console.log(arguments); //["800",5,"12309800"]、
});
a.replace(/([1-9])0+$/,function(){
    console.log(arguments); //["800","8",5,"12309800"]
});
a.replace(/([1-9])(0+$)/,function(){
    console.log(arguments); //["800","8","00",5,"12309800"]
});
a.replace(/(([1-9])(0+$))/,function(){
    console.log(arguments); //["800","800","8","00",5,"12309800"]
});複製程式碼

回撥函式的arguments陣列部分組成:[完整匹配的字串,$1,$2,...,$n,匹配的開始位置,原始字串],$1...$n表示每個括號對的匹配,規則和前面的相同。
所以有一下規律:

let arr = [...arguments], len = arr.length;
(len >= 3) === true;
arr[0] = 完整匹配的字串;
arr[len-2] = 匹配的開始位置;
arr[len-1] = 原始字串;複製程式碼

注意:除了匹配的開始位置是Number型別外,其餘的都是String型別

非常規型別引數

如果引數型別不是上述兩種情況,會發生什麼呢?看看下面的例子:

a.replace(0,null); //123null9800
a.replace(0,undefined); //123null9800
a.replace(0,[]); //1239800
a.replace(0,Array); //1230,3,123098009800
b.replace({},5); //123098005
c.replace({},5); //'12309800{}'
a.replace(0,{}); //123[object Object]9800
a.replace(0,Object); //12309800複製程式碼

由上面的例子可以看出,如果非正則也非字串,則有以下規則:
null變數,則會轉換成'null'字串;
undefined變數,則會轉換成'undefined'字串;
[]變數,則會呼叫join()方法轉換成字串,預設以,分割,值得注意的是空陣列將會被轉換成空字串(沒有任何字元),通常會被匹配源字串的開始位置(預設開始位置為空字串);
'Array'變數,則會先轉成成一個匹配的陣列,形如[完整匹配的字串,$1,$2,...,$n,匹配的開始位置,原始字串],然後對它呼叫join()方法轉換成字串,預設以,分割;
{}變數,則會呼叫Object.protype.toString.call()方法把{}轉換成[object Object];
Object變數,則貌似什麼都沒做

雖然可以傳入這些非正常引數,但大多數情況下這些型別的引數對實際是毫無意義的,所以不建議傳入以上型別的引數。同上面的正則匹配分割規則一樣,為了方便使用稱呼,姑且我把上面的轉換規則稱為正則匹配引數轉換規則

match(rule[regex/substr])

match方法可在字串內檢索指定的值,或找到一個或多個正規表示式的匹配。
該方法類似indexOflastIndexOf,但是它返回指定的值,而不是字串的位置;

引數

引數的傳遞除了常規的正則和字串以外,其餘所有型別的引數都會按照上述的正則匹配引數轉換規則轉換成字串形式來匹配。

返回值

返回值根據傳入的引數型別和規則的不同,返回的內容不同,但總體來說,它是返回一個物件,而不是索引,如果沒匹配到任何符合條件的字串,則返回null

非全域性匹配正則

如果匹配規則是一個非全域性匹配規則,那麼,它此時的返回值是一個偽陣列物件(likeArr),形如:[一個展開的匹配到的字串陣列, 匹配到的字串位置, 原始字串],它有如下規律:

var likeArr = a.match(regex);
likeArr[0] = 匹配到的字串;
likeArr[1...n] = 正則匹配分割規則匹配的字串;
likeArr.index = 匹配到字串的位置
likeArr.inupt = 原始字串複製程式碼

看例子:

a.match(/[1-9]0+$/); //[0:'800',index:5,input:'12309800']
a.match(/([1-9])0+$/); //[0:'800',1:'8',index:5,input:'12309800']
a.match(/[1-9](0+$)/); //[0:'800',1:'00',index:5,input:'12309800']
a.match(/([1-9])(0+$)/); //[0:'800',1:'8',2:'00',index:5,input:'12309800']複製程式碼

全域性匹配正則

如果匹配規則是一個全域性匹配規則(正在攜帶有g標誌),那麼,它此時的返回值是一個陣列物件(arr),形如:[匹配到的字串數1,匹配到的字串數2,匹配到的字串數3];
看例子:

a.match(/[1-9]0/); //[0:'30',index:2,input:'12309800']
a.match(/[1-9]0/g); //[0:'30',1:'80']複製程式碼

search(rule[regex/substr])

search方法用於檢索字串中指定的子字串,或檢索與正規表示式相匹配的子字串。
stringObject中第一個與rule相匹配的子串的起始位置。如果沒有找到任何匹配的子串,則返回-1
注意:

  • search方法不執行全域性匹配,它將忽略標誌g
  • 忽略regexplastIndex屬性,總是從字串的開始進行檢索,這意味著它總是返回stringObject的第一個匹配的位置

同樣,search可以傳入任何引數型別,它會遵循正則匹配引數轉換規則進行轉換

split(rule[regex/substr],len)

這個方法就不用多說,很常用的字串分割方法。
第二個引數的作用就是限制返回值的長度,表示返回值的最大長度

當然,它依然可以傳入任何引數型別,會遵循正則匹配引數轉換規則進行轉換

有一段加密的後的密碼,我們需要分離出字串'12a344gg333tt445656ffa6778ii99'中的前三組數字,通過某種計算才能得出正確的密碼

'12a344gg333tt445656ffa6778ii99'.split(/[a-zA-Z]+/g,3);//['12','334','333']複製程式碼

最後

寫了這麼多,突然發現以前僅僅是在用這些方法,瞭解得很不夠深入。越是學習才發現其中的奧祕!學無止境,與諸君共勉!
以上內容如有錯誤之處,希望諸君不吝指出!

相關文章