這些是本人在 github.pages 上寫的部落格,歡迎大家關注和糾錯,本人會定期在github pages 上更新。有想要深入瞭解的知識點可以留言。
同時,這是本人第一次寫文章,如有目錄結構不合理,還請指出。
前言
剛開始學習 JS 時,正規表示式一直是我不願意面對的,每次讀到有關正規表示式的時候,都會避而遠之。可是,一次,當我開啟 JQ 原始碼的時候,發現裡面有大量的正規表示式。於是乎,自己就強迫自己學習正則,學習的過程還是蠻愉快的。最後,真香定律終於出現了。哈哈哈!!
這篇教程我會由淺入深的來和大家分享正規表示式,讓大家即學習到正規表示式的用法,也瞭解其在 JS 中表現的不為人知的一面。
概述
正規表示式是 JS 中很重要的一環。也是對很多人比較不願意面對的一個知識點。但是,當我們真正掌握了正規表示式,可以利用其在我們的程式碼中發揮很大的威力,大大的簡化我們的程式碼。對於喜歡閱讀一些庫原始碼的夥伴。這個真的是必須掌握的。
當然,正則基本在每個語言都有實現。雖不能說都相同。但是基本上都是大同小異。此外,正規表示式的範圍非常的廣,這裡也不可能每個知識點都會涉及到。這裡,作者會將一些我認為常見的,重要的,常見的注意點給大家一一分析。
正規表示式基礎概念
定義
正規表示式,英文叫做 Regular Expression。按照英文字面意思解釋,就是有規則的表示式,正規表示式就是由一些列的語法規則組成的字串,然後按照這種組合的規則去匹配一些字串,篩選出符合條件的字串。
知識瞭解:根據 ECMA5 規範,JS 中正規表示式的格式和功能是以 perl 語言為藍本的。
我們平時寫的正規表示式很不直觀,這裡推薦一個線上工具。該工具以視覺化的介面來描述我們寫的正規表示式。工具比較簡單,大家自行了解。 線上工具
正則表示方法的區別
正規表示式的表示方法有兩種
- 字面量表示法
- 建構函式表示法
let reg = /text/ig
let regExp = new RegExp('text', ig)
複製程式碼
兩種表示方法都可行。區別在於:利用建構函式進行表示的時候,可以動態的構建正規表示式的規則。
let str = String('****')
// 如 let className = str + 'name'
regExp = new RegExp(className, 'ig')
複製程式碼
正規表示式的組合規則太多了,大家可以去看一下 ECMA 規範。下面我們就介紹一些常用的,經常遇到的情況。
正規表示式組成元素
正規表示式的組成一般由以下幾類構成
- 原義文字字元
- 元字元
- 修飾詞
修飾詞
在 JavaScript 中,全域性修飾詞有 g、i、m、y、u、s
這些修飾詞在正規表示式的匹配中,起到了很重要的作用。
g: 代表全域性匹配,當匹配到目標字串的時候,不會停止匹配,而會繼續匹配。直到匹配結束為止。
i: 匹配的過程中,忽略大小寫
m: 換行匹配。字串可以換行,如果當前行沒有匹配到,可以換行繼續匹配
y: 執行“粘性”搜尋,匹配從目標字串的當前位置開始
u: 相當於將匹配模式轉換成 unicode 模式,可以正確處理大於\uFFFF的 Unicode 字元。大家可以自行參考 瞭解
s: 我們知道 元字元 . 代表除了回車換行符以外的所有字元,但是加上 s 修飾符後, . 可以匹配任意字元
/./.text('\n') // false
/./s.test('\n') // true
//其實還有另一種技巧可以匹配所有字元
/[^]/.test('\n') // true
複製程式碼
y 代表什麼含義? 這個等會再做解釋,先簡單說下,這個與正規表示式的一個屬性 lastlndex 有關係,現在解釋,可能會一臉懵逼。 下面會與 g 標誌 一起討論。
元字元
元字元是在正規表示式中有特殊含義的非字母字元。這些元字元使得正規表示式的組合規則十分強大。
// 引用自規範原文 我們可以看一下這些元字元都有哪些,我們應該很熟悉這其中代表的含義。
PatternCharacter :: SourceCharacter but not any of:
^ $ \ . * + ? ( ) [ ] { } |
此外,還有一些字元,是組合而成的,代表特殊的含義
有 \b、\B、\d、\D、\w、\W、\s、\S、\f、\v、\t、\n、\r 等等。這些字元代表的含義,大家自行了解,這裡不一一講解這些元字元的含義。
下面舉例子會用到一些,會對其含義做說明。
複製程式碼
既然元字元代表這麼多的含義,那麼我們如果需要在字串中匹配這些字元怎麼辦呢? 這個時候,可以使用轉義字元幫忙。
舉個栗子:
reg = /\d/ // \d 是元字元,表示數字,這個正規表示式只能匹配數字,如果我們需要匹配'\d'呢
reg.test('\d') // false
這時候需要用轉義字元轉義
reg = /\\d/ // \ 代表轉義字元
reg.test('\d') // 還是false
這個為什麼還是 false 呢,不是按照規矩辦事嗎?
這是因為 JS 裡的字串也有轉義字元!
可以試一下 '\d'.length 等於1,這個時候要想匹配 '\d' 必須要在字串中對其轉義
reg.test('\\d') // true
複製程式碼
總結:遇到這種情況,別總是忙著為正規表示式轉義,還得為字串轉義,關於字串裡面的轉義字元,這超過了本篇討論範圍,大家自行了解。
常見元字元的解釋
下面介紹一些常用的元字元
元字元 | 表示及含義 | 解釋 |
---|---|---|
. | /[ ^\r\n ]/ | 除了回車換行以外的全部字元 |
\d | /[0-9]/ | 數字字元 |
\D | /[^0-9]/ | 非數字字元 |
\w | /[0-9a-zA-Z_]/ | 單詞字元(數字,字母,下劃線) |
\W | /[^0-9a-zA-Z_]/ | 代表非單詞字元 |
\s | /[\f\n\r\t\v\u00a0\u1680\u180e \u2000-\u200a\u2028\u2029\u202f \u205f\u3000\ufeff]/ |
空白字元,包括空格、 製表符、換頁符和換行符 |
\S | /[^\s]/ | 非空白字元 |
| | /x|y/ | x or y |
\b | word boundary | 單詞邊界 |
\B | none word boundary | 非單詞邊界 |
^ | /^abc/ | 匹配以abc為開始的字串 |
$ | /abc$/ | 匹配以abc為結束的字串 |
字元類
我們都知道用特定的正規表示式去匹配特定的字串很輕鬆。因為不會出現其他情況,邏輯上講是非常清晰的事情。
舉個栗子:
let reg = /abc\b/
// 如下圖 表示匹配 abc後面跟上單詞邊界。
reg.test('abc bcd') // true
reg.test('abcc') //false 因為abc後面緊跟單詞邊界
複製程式碼
可是,有時候我們的需求不是匹配特定字元,而是要匹配符合一些特性的字元。比如,需要匹配 a b c 任意一個,存在即滿足條件。
簡而言之:我只要你們中的一個出現就OK。
這個時候,我們就可以使用元字元 [] 來構建這樣一個 字元類。
這裡的類,我們可以聯想到程式語言的類,泛指一些符合特性的事物,而不是特指。
舉個例子:
let reg = /[abc]\b/
// 如下圖 表示 one of abc 後面緊跟單詞邊界就滿足條件
reg.test('a') // true
複製程式碼
字元類很強大,但是,如果我們的需求是要匹配除了一個字元類之外的字元呢?
簡而言之:別人都行,就你們不可以。
這個時候,我們可以使用字元類的反向類,使用元字元 ^ 在寫好的字元類裡面取反。
舉個例子:
let reg = /[^abc]\b/
// 如下圖 表示 None of abc 後面緊跟單詞邊界就滿足條件
reg.test('e') // true
複製程式碼
解釋一下單詞邊界的含義:單詞邊界這個概念,很多人都比較模糊。我只能說一下我的理解 在正規表示式中,\w 代表單詞字元,\W 代表非單詞字元,只要單詞字元緊挨著非單詞字元,那麼在這兩者中間,就存在單詞邊界。
範圍類
字元類給我們注入了一種全新的功能,類似於資料庫的模糊查詢。我們可以利用這一功能匹配範圍內的字串了。 但是,應用場景多了,也會出現問題。
舉個例子:
//我們想要匹配 數字1到8中的任意一個,我們利用字元類的概念可以這樣寫
reg = /[12345678]/
// 可能有的小夥伴可以接受,那好,如果我們想要匹配小寫字母,a 到 z 的任意一個字元
reg = /[abcdefghijklmnopqrstuvwxyz]/ // 這樣一坨,寫的很難受
複製程式碼
看上面的例子,寫程式碼的難受,讀程式碼的估計也不舒服。這時候,我們需要範圍類幫忙。
所謂的範圍類,就是匹配具有一定規則的一段範圍之內的字元:
使用字元 -,來達成這一目標
舉個例子:
// 匹配 a 到 z 的任意一個字元
reg = /[a-z]/
// 匹配除了 a 到 z 的任意一個字元
reg = /[^a-z]/
// 常見的模式
/\d/ 就相當於 /[0-9]/
/\D/ 就相當於 /[^0-9]/
/\w/ 就相當於 /[a-zA-Z0-9]/
/\W/ 就相當於 /[^a-zA-Z0-9]/
複製程式碼
一個問題:**-**不是元字元,是否可以在範圍類中匹配?如果可以,是否需要轉義或者其他特殊操作。
匹配該字元在字元類中是可以的,但是有注意點:即 - 只可以放在範圍類的開頭或結尾,才會匹配該字元。
不可以出現在兩個字元中間,不然,該字元還是會被當作範圍類中的特殊字元來對待
舉個例子:
let reg = /[1-z]/
reg.test('-') // false
reg.test('a') // true
let reg = /[1-9-]/
reg.test('-') // true
reg.test('1') // true
複製程式碼
量詞
我們之前介紹的字元類或非字元類,只能匹配特定類出現一次,如果出現多次,需要額外再寫相同的程式碼進行匹配。
舉個例子:
// 需求:匹配有連續5個數字的字串
let reg = /\d\d\d\d\d/ // 那如果要匹配出現連續3至6個數字的字串呢?
let reg = /\d\d\d|\d\d\d\d|\d\d\d\d\d|\d\d\d\d\d\d/ // 這樣寫太複雜,我需要更簡單的寫法
複製程式碼
有這樣的需求時,我們就需要量詞來幫忙。
量詞有幾種表示的方式,各自代表不同的需求。
量詞的表示有以下這幾種表示:
+ 表示匹配一次或一次以上。one or more
? 表示匹配0次或一次。 one or less
* 表示匹配0次或0次以上。none or onemore
{m,n} 表示匹配 m 到 n 次。[m,n]閉區間 m less n most
{m,} 表示匹配至少 m 次。 m less
{m} 表示匹配出現 m 次
如果要表示至多出現 m 次,可以這樣表示 {0,m}
重寫例子:
reg = /\d{5}/ // 匹配有5個連續數字的字串
reg = /\d{3,6}/ // 匹配有連續 3 至 6 個數字的字串
複製程式碼
大家可以在圖形化工具裡面自己嘗試下,很直觀。
正規表示式的貪婪模式
注意:這裡,我們會先應用 String.prototype.replace 方法來很形象的解釋正規表示式的貪婪模式。
來看一下這樣等應用場景:我們需要匹配連續 5-10 個小寫字母等場景。目標字串滿足這個需求,但是匹配等結果是什麼?
是匹配到5個字母就不匹配還是繼續匹配更多等字母直到匹配失敗呢?
人是貪婪的,所以人設計的正規表示式也是貪婪的。
在正規表示式中,會盡可能的匹配更多的字元,直到匹配失敗為止。
舉個例子:
reg = /[a-z]{5,10}/
str = 'ahhsjkiosbsasdasllk' // str.length === 19
我們用 replace 方法來驗證一下,正規表示式匹配了多少字元
str1 = str.replace(reg, 'Q') // str1.length === 10
str1 = 'Qsasdasllk'
複製程式碼
上述的例子,我們可以看出,正規表示式是屬於貪婪模式。那麼我們現在想要取消貪婪模式,可以嗎?
可以,只需要在量詞後加上**元字元?**就可以取消貪婪模式啦。
舉個例子:
reg = /[a-z]{5,10}?/
str = 'ahhsjkiosbsasdasllk' // str.length === 19
我們用 replace 方法來驗證一下,正規表示式匹配了多少字元
str1 = str.replace(reg, 'Q') // str1.length === 15
str1 = 'Qkiosbsasdasllk'
複製程式碼
分組
假如,我們現在需要這樣一個需求,需要匹配包含'mistyyyy'連續重複2次的字串。
這種情況,我們按照之前的寫法可能會這樣寫。
/mistyyyy{2}/
複製程式碼
但是,這樣匹配是錯誤的,這表達的意思是y重複2次,而不是 mistyyyy 重複2次
這個時候,我們可以使用分組這個概念來幫助我們。
用法:將需要分組的資訊,用元字元()包含起來。這樣就可以使量詞作用於分組了。
reg = /(mistyyyy){2}/
str = 'Im mistyyyymistyyyymistyyyy yeah'
reg.test(str) // true
如下圖所示
複製程式碼
再看一個例子,這時候,我要改名字了。mistyyyy 或者 missyyyy 都是可以的。那我們怎麼匹配它呢?
//我們可以這樣寫
reg = /mistyyyy|missyyyy/
但是利用分組,我們可以這樣寫
reg = /mis(s|t)yyyy/
reg.test('mistyyyy') // true
reg.test('missyyyy') // true
複製程式碼
捕獲和非捕獲分組
現在我們來看一個,平時開發中經常出現的需求。如:將日期 '2018-12-23' 轉換為 '23/12/2018'
這個時候,我們就很頭大了。單純的匹配到這個日期並不困難。但是如何將其轉換這就成了難點。當然,我們可以進行最原始的方法進行解決。
reg = /\d{4}-\d{2}-\d{2}/
'2018-12-23'.replace(reg, '23/12/2018')
// 這樣的程式基本沒有靈活性。
複製程式碼
這時候,我們要講的捕獲就要出現了。前面講到了分組,既然可以分組,那我們也可以捕獲分組。
捕獲分組又可以稱為引用:
- 一種是正向引用,用於正規表示式裡面的表示。
- 另一種稱為反向引用,常用於正規表示式匹配結果的替換。
我們先看正向引用,舉個最適合的例子。
//我們現在需要匹配一個 dom 節點,比如匹配 id 為 container 的 div dom 節點。
let domContainer =
`<div>
<div id="container">
this is container
</div>
</div>`
reg = /<div id="container">([^<\/]+)<\/div>/m
domContainer.replace(reg, 's') // <div>s</div>
複製程式碼
這個時候,我們可以使用正向引用,即 \1 代表第一個分組的引用, \2 代表第二個分組的引用等等 以此類推
我們來重寫正規表示式。
reg = /<(div) id="container">([^<\/]+)<\/\1>/m
// 大家可以試一下,同樣的效果。
複製程式碼
介紹完正向引用,我們來看一下反向引用。
在正規表示式進行分組時,當匹配結束時候,我們希望可以以分組為單位進行字串的替換,這樣可行嗎?
舉個例子
reg = /(\d{4})-(\d{2})-(\d{2})/
// 這樣我們就把正則寫好了。考慮到之前的例子,我們需要將第三個分組放在開頭,第二個分組位置不變,第一個分組放在最後
// 如何做
'2018-12-23'.replace(reg, '$3/$2/$1') // "23/12/2018"
複製程式碼
由上述例子可以得知,反向引用就是用 '$1' 獲取第一個分組 '$2' 獲取第二個分組...以此類推
注意;是 '$1' 代表一個分組,而不是 $1,這裡需要注意一下
有時候,我們根本不需要捕獲一個分組,就如剛才 reg = /mis(s|t)yyyy/ 一種情況。我們只是想用分組實現一下 或 操作。 沒有分組的必要,其次,當正規表示式變得複雜起來,保持明顯的分組是很有必要的。
這個時候,我們可以在分組的括號裡面加上 ?: 這樣就可以取消捕獲了
reg = /mis(s|t)yyyy/
'mistyyyy'.replace(reg, '$1') // 't'
// 取消捕獲
reg = /mis(?:s|t)yyyy/
'mistyyyy'.replace(reg, '$1') // '$1'
複製程式碼
我們可以看下圖片的比較,沒有分組了。說明取消了捕獲。
正向匹配和反向匹配
先解釋這兩個概念,我們都知道在 JavaSCript 中,正規表示式匹配的順序是順著目標字串進行匹配。
如果我們需要設計一些帶條件的匹配規則,比如說:我們需要匹配字串 'mistyyy' 後面必須是 'good'
舉個例子:
reg = /mistyyyygood/
複製程式碼
這個時候,'mistyyyy' 後面是 'good' 但是此時,'good' 也被匹配到了,如果我們用 replace 做替換,那麼 good 也會被替換掉。
要滿足這樣的條件。我們可以使用正向匹配
規則如下:
- /expression(?=condition)/ 這表示expression後面必須緊跟 condition 作為條件。
- /expression(?!condition)/ 這表示expression後面必須不是 condition 作為條件。
舉個例子;
str = 'mistyyyygood boy'
/mistyyyy(?=good)/.test(str) // true
str.replace(/mistyyyy(?=good)/, 'you') // yougood boy
/mistyyyy(?!good)/.test(str) // false
複製程式碼
注意:此時,condition 只是作為條件進行篩選,並不會被匹配到。
我們可以看一個例子
str = 'mistyyyygood boy'
str.replace(/mistyyyy(?=good)/, '$1') // $1good boy
// 我們可以看出條件是不會被匹配到到。
複製程式碼
反向匹配,與正向匹配的規則相反,該特性是 ES 2018 新加的特性。
規則如下:
- /(?<=condition)expression/ 表示 expression 前必須滿足 condition 條件才匹配
- /(?<!condition)expression/ 表示 expression 前必須不滿足 condition 條件才匹配
詳解 regExp 物件屬性
我們隨便寫一個正則,看一下列印出來的正規表示式的屬性有哪些
reg = /\u0002/yimgus
{
dotAll: true,
flags: 'gimsuy',
global: true,
ignoreCase: true,
lastIndex: 0,
multiline: true,
source: '\u0002',
sticky: true,
unicode: true,
__proto__: Object
}
複製程式碼
dotAll,global,ignoreCase,multiline,stricky,unicode 分別代表修飾詞 s,g,i,m,y,u 是否出現在正在表示式的修飾詞位置。
flags 表示出現的修飾詞。
source 表示正規表示式的規則主體部分。
lastIndex 個人認為最重要的屬性就是該屬性。下面會圍繞該屬性展開擴充一下。
我們先看個奇怪的例子:
reg = /\d{2}/g
str = '12sd'
reg.lastIndex // 0
reg.test(str) // true
reg.lastindex // 2
reg.test(str) // false
reg.lastindex // 0
複製程式碼
lastIndex的值型別是 number 型別。可以進行讀寫操作。
舉個例子:
reg = /\d{2}/
reg.lastindex // 0
reg.lastIndex = 2
reg.lastindex // 2
複製程式碼
該屬性的含義是從目標字串的 lastIndex 位置開始進行匹配。但是這是有限制的,只有當修飾符存在 g 或者 y 的時候,才會起作用。
舉個例子:
reg = /\d{2}/
str = '12sd'
reg.lastIndex = 2
reg.test(str) // true
reg.lastIndex // 2
reg =/\d.\d/s
str = '1\n2'
reg.lastIndex = 2
reg.test(str) // true
reg.lastIndex // 2
reg = /\d{2}/g
str = '12sd'
reg.lastIndex = 2
reg.test(str) // false
reg.lastIndex // 0
reg = /\d{2}/y
str = '12sd'
reg.lastIndex = 2
reg.test(str) // false
reg.lastIndex // 0
複製程式碼
通過以上的例子,我們可以看出,lastIndex 屬性隻影響了 修飾符 g 和 修飾符 y 的匹配結果。
也就是說:只有這兩種的形式是從目標字串的 lastIndex 位置進行匹配的,其他的修飾符會忽略這個屬性。
而且,這兩種修飾符匹配失敗了,lastIndex 會重置為0。
由此可見,g 修飾符 和 修飾符 y 有相同的作用。那麼我們來探尋一下他們的異同點。
相同點:
- 他們都會受 lastIndex 的屬性值影響正則開始匹配的效果。即影響從目標字串的何處開始匹配
- 他們在匹配失敗後 lastindex 的屬性都會置為 0。
- 他們匹配成功後,lastIndex 都會重置為匹配成功的字串的下一個字元。
- 修飾符 y 和 g 都不受元字元 ^ 從目標字串開始的限制,它只受限於 lastIndex 的位置開始匹配。並且只從 lastIndex 的位置開始匹配。
reg1 = /\d/g
reg2 = /\d/y
str = '1ssss'
reg1.lastIndex = reg2.lastIndex = 1
reg1.test(str) // false
reg2.test(str) // false
reg1.lastindex // 0
reg2.lastindex // 0
// 說明都受 lastIndex 的影響,且匹配失敗都會置為0
reg1.test(str) // true
reg2.test(str) // true
reg1.lastIndex // 1
reg2.lastIndex // 1
// 匹配成功後,lastIndex 都會重置為匹配成功的字串(chharAt(0))的下一個字元
reg1 = /^\d/g
reg2 = /^\d/y
str = '1ssss1'
reg1.lastIndex = reg2.lastIndex = 1
reg1.test(str) // false
reg2.test(str) // false
複製程式碼
不同點:
修飾符 g 是全域性匹配,即匹配到目標字串不會停止,繼續匹配下去,直到沒有找到所有符合規則的為止。修飾符 y 不是全域性匹配,找到符合規則的就會停止。
舉個例子:
reg1 = /\d/g
reg2 = /\d/y
str = '1sssss1'
reg1.test(str) // true
reg1.lastIndex // 1
reg1.test(str) // true
reg1.lastIndex // 7
reg2.test(str) // true
reg2.lastindex // 1
reg2.test(str) // false
reg2.lastindex // 0
// 說明 修飾符 g 是全域性匹配,而 y 不是全域性匹配。
複製程式碼
總結
關於正規表示式,我們講了一些常見的語法和一些比較生澀的疑難點。對於正規表示式,我們掌握了這些知識點,並不能完全發揮其應有的實力。
我們還應該掌握,應用正則的一些方法有:String 型別的 split,replace,match,search 。RegExp 型別的 test,exec 方法。
真正瞭解這些方法的應用,才能讓正規表示式的強大威力。這些方法,這裡暫時不講了,小夥伴們應該熟悉這些方法的使用,利用他們組合正規表示式,展示出強大的威力。
此外,正規表示式常見的應用場景有模版的解析,dom節點的提取分離,這裡面含有大量複雜的正規表示式。如果小夥伴需要精通掌握正規表示式,可以閱讀sizzle這個庫,該庫是 JQ 的核心部分,專門處理複雜的 dom selector,希望我們可以繼續努力,將正規表示式真正的掌握。這樣,我們就可以寫出更加優雅,更加具有可讀性的程式碼了。