淺談JavaScript正規表示式

丁春雷發表於2018-12-24

這些是本人在 github.pages 上寫的部落格,歡迎大家關注和糾錯,本人會定期在github pages 上更新。有想要深入瞭解的知識點可以留言。

同時,這是本人第一次寫文章,如有目錄結構不合理,還請指出。

前言

剛開始學習 JS 時,正規表示式一直是我不願意面對的,每次讀到有關正規表示式的時候,都會避而遠之。可是,一次,當我開啟 JQ 原始碼的時候,發現裡面有大量的正規表示式。於是乎,自己就強迫自己學習正則,學習的過程還是蠻愉快的。最後,真香定律終於出現了。哈哈哈!!

這篇教程我會由淺入深的來和大家分享正規表示式,讓大家即學習到正規表示式的用法,也瞭解其在 JS 中表現的不為人知的一面。

概述

正規表示式是 JS 中很重要的一環。也是對很多人比較不願意面對的一個知識點。但是,當我們真正掌握了正規表示式,可以利用其在我們的程式碼中發揮很大的威力,大大的簡化我們的程式碼。對於喜歡閱讀一些庫原始碼的夥伴。這個真的是必須掌握的。

當然,正則基本在每個語言都有實現。雖不能說都相同。但是基本上都是大同小異。此外,正規表示式的範圍非常的廣,這裡也不可能每個知識點都會涉及到。這裡,作者會將一些我認為常見的,重要的,常見的注意點給大家一一分析。

正規表示式基礎概念

定義

正規表示式,英文叫做 Regular Expression。按照英文字面意思解釋,就是有規則的表示式,正規表示式就是由一些列的語法規則組成的字串,然後按照這種組合的規則去匹配一些字串,篩選出符合條件的字串。

知識瞭解:根據 ECMA5 規範,JS 中正規表示式的格式和功能是以 perl 語言為藍本的。

我們平時寫的正規表示式很不直觀,這裡推薦一個線上工具。該工具以視覺化的介面來描述我們寫的正規表示式。工具比較簡單,大家自行了解。 線上工具

正則表示方法的區別

正規表示式的表示方法有兩種

  1. 字面量表示法
  2. 建構函式表示法

    let reg = /text/ig

    let regExp = new RegExp('text', ig)

複製程式碼

兩種表示方法都可行。區別在於:利用建構函式進行表示的時候,可以動態的構建正規表示式的規則。


    let str = String('****')
    // 如 let className = str + 'name'
    regExp = new RegExp(className, 'ig')

複製程式碼

正規表示式的組合規則太多了,大家可以去看一下 ECMA 規範。下面我們就介紹一些常用的,經常遇到的情況。

正規表示式組成元素

正規表示式的組成一般由以下幾類構成

  1. 原義文字字元
  2. 元字元
  3. 修飾詞

修飾詞

在 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後面緊跟單詞邊界

複製程式碼

淺談JavaScript正規表示式

可是,有時候我們的需求不是匹配特定字元,而是要匹配符合一些特性的字元。比如,需要匹配 a b c 任意一個,存在即滿足條件。

簡而言之:我只要你們中的一個出現就OK。

這個時候,我們就可以使用元字元 [] 來構建這樣一個 字元類

這裡的類,我們可以聯想到程式語言的類,泛指一些符合特性的事物,而不是特指。

舉個例子:


    let reg = /[abc]\b/
    // 如下圖 表示 one of abc 後面緊跟單詞邊界就滿足條件
    reg.test('a') // true

複製程式碼

淺談JavaScript正規表示式

字元類很強大,但是,如果我們的需求是要匹配除了一個字元類之外的字元呢?

簡而言之:別人都行,就你們不可以。

這個時候,我們可以使用字元類的反向類,使用元字元 ^ 在寫好的字元類裡面取反。

舉個例子:


    let reg = /[^abc]\b/
    // 如下圖 表示 None of abc 後面緊跟單詞邊界就滿足條件
    reg.test('e') // true

複製程式碼

淺談JavaScript正規表示式

解釋一下單詞邊界的含義:單詞邊界這個概念,很多人都比較模糊。我只能說一下我的理解 在正規表示式中,\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]/

複製程式碼

淺談JavaScript正規表示式

一個問題:**-**不是元字元,是否可以在範圍類中匹配?如果可以,是否需要轉義或者其他特殊操作。

匹配該字元在字元類中是可以的,但是有注意點:即 - 只可以放在範圍類的開頭或結尾,才會匹配該字元。

不可以出現在兩個字元中間,不然,該字元還是會被當作範圍類中的特殊字元來對待

舉個例子:


    let reg = /[1-z]/
    reg.test('-') // false
    reg.test('a') // true

    let reg = /[1-9-]/
    reg.test('-') // true
    reg.test('1') // true

複製程式碼

淺談JavaScript正規表示式

量詞

我們之前介紹的字元類或非字元類,只能匹配特定類出現一次,如果出現多次,需要額外再寫相同的程式碼進行匹配。

舉個例子:


    // 需求:匹配有連續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
    如下圖所示

複製程式碼

淺談JavaScript正規表示式

再看一個例子,這時候,我要改名字了。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')
    // 這樣的程式基本沒有靈活性。

複製程式碼

這時候,我們要講的捕獲就要出現了。前面講到了分組,既然可以分組,那我們也可以捕獲分組。

捕獲分組又可以稱為引用:

  1. 一種是正向引用,用於正規表示式裡面的表示。
  2. 另一種稱為反向引用,常用於正規表示式匹配結果的替換。

我們先看正向引用,舉個最適合的例子。


    //我們現在需要匹配一個 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正規表示式
淺談JavaScript正規表示式

正向匹配和反向匹配

先解釋這兩個概念,我們都知道在 JavaSCript 中,正規表示式匹配的順序是順著目標字串進行匹配。

如果我們需要設計一些帶條件的匹配規則,比如說:我們需要匹配字串 'mistyyy' 後面必須是 'good'

舉個例子:


    reg = /mistyyyygood/    

複製程式碼

這個時候,'mistyyyy' 後面是 'good' 但是此時,'good' 也被匹配到了,如果我們用 replace 做替換,那麼 good 也會被替換掉。

要滿足這樣的條件。我們可以使用正向匹配

規則如下:

  1. /expression(?=condition)/ 這表示expression後面必須緊跟 condition 作為條件。
  2. /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 新加的特性。

規則如下:

  1. /(?<=condition)expression/ 表示 expression 前必須滿足 condition 條件才匹配
  2. /(?<!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 有相同的作用。那麼我們來探尋一下他們的異同點。

相同點:

  1. 他們都會受 lastIndex 的屬性值影響正則開始匹配的效果。即影響從目標字串的何處開始匹配
  2. 他們在匹配失敗後 lastindex 的屬性都會置為 0。
  3. 他們匹配成功後,lastIndex 都會重置為匹配成功的字串的下一個字元。
  4. 修飾符 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,希望我們可以繼續努力,將正規表示式真正的掌握。這樣,我們就可以寫出更加優雅,更加具有可讀性的程式碼了。

相關文章