正則表達式-JavaScript

  • 2019 年 12 月 5 日
  • 筆記

什麼是正則表達式

正則表達式是用於匹配字符串中字符組合的模式。在 JavaScript中,正則表達式也是對象。 這些模式被用於 RegExpexectest 方法, 以及 Stringmatchreplacesearchsplit 方法。

正則表達式存在於大部分的編程語言,就算是在寫shell時也會不經意的用到正則。 比如大家最喜歡的rm -rf ./*,這裡邊的*就是正則的通配符,匹配任意字符。

JavaScript也有正則表達式的實現,差不多就長這個樣子:/d/(匹配一個數字)。 個人認為正則所用到的地方還是很多的,比如模版字符的替換、解析URL,表單驗證 等等一系列。 如果在Node.js中用處就更為多,比如請求頭的解析、文件內容的批量替換以及寫爬蟲時候一定會遇到的解析HTML標籤。

正則表達式在JavaScript中的實現

JavaScript中的語法

贅述那些特殊字符的作用並沒有什麼意義,浪費時間。 推薦MDN的文檔:基礎的正則表達式特殊字符

關於正則表達式,個人認為以下幾個比較重要:

貪婪模式與非貪婪模式

P.S. 關於貪婪模式非貪婪模式,發現有些地方會拿這樣的例子:

/.+/ // 貪婪模式  /.+?/ // 非貪婪模式

僅僅拿這樣簡單的例子來說的話,有點兒扯淡

// 假設有這樣的一個字符串  let html = '<p><span>text1</span><span>text2</span></p>'    // 現在我們要取出第一個`span`中的文本,於是我們寫了這樣的正則  html.match(/<span>(.+)</span>/)  // 卻發現匹配到的竟然是 text1</span><span>text2  // 這是因為 我們括號中寫的是 `(.+)` .為匹配任意字符, +則表示匹配一次以上。  // 當規則匹配到了`text1`的時候,還會繼續查找下一個,發現`<`也命中了`.`這個規則  // 於是就持續的往後找,知道找到最後一個span,結束本次匹配。    // 但是當我們把正則修改成這樣以後:  html.match(/<span>(.+?)</span>/)  // 這次就能匹配到我們想要的結果了  // `?`的作為是,匹配`0~1`次規則  // 但是如果跟在`*`、`+`之類的表示數量的特殊字符後,含義就會變為匹配盡量少的字符。  // 當正則匹配到了text1後,判斷後邊的</span>命中了規則,就直接返回結果,不會往後繼續匹配。

簡單來說就是:

  1. 貪婪模式,能拿多少拿多少
  2. 非貪婪模式,能拿多少拿多少

捕獲組

/123(d+)0/ 括號中的被稱之為捕獲組。

捕獲組有很多的作用,比如處理一些日期格式的轉換。

let date = '2017-11-21'    date.replace(/^(d{4})-(d{2})-(d{2})$/, '$2/$3/$1')

又或者可以直接寫在正則表達式中作為前邊重複項的匹配。

let template = 'hello helloworl'  template.match(/(w+) 1/) // => hello hello    // 我們可以用它來匹配出month和day數字相同的數據  let dateList = `  2017-10-10  2017-11-12  2017-12-12  `    dateList.match(/^d{4}-(d{2})-(1)/gm) // => ["2017-10-10", "2017-12-12"]

非捕獲組

我們讀取了一個文本文件,裡邊是一個名單列表 我們想要取出所有Stark的名字(但是並不想要姓氏,因為都叫Stark),我們就可以寫這樣的正則:

let nameList = `  Brandon Stark  Sansa Stark  John Snow  `    nameList.match(/^w+(?=s?Stark)/gm) // => ["Brandon", "Sansa"]

上邊的(?=)就是非捕獲組,意思就是規則會被命中,但是在結果中不會包含它。

比如我們想實現一個比較常用的功能,給數組添加千分位:

function numberWithCommas (x = 0) {    return x.toString().replace(/B(?=(d{3})+(?!d))/g, ',')  }    numberWithCommas(123) // => 123  numberWithCommas(1234) // => 1,234

B代表匹配一個非單詞邊界,也就是說,實際他並不會替換掉任何的元素。 其次,後邊的非捕獲組這麼定義:存在三的倍數個數字(3、6、9),並且這些數字後邊沒有再跟着其他的數字。 因為在非捕獲組中使用的是(d{3})+,貪婪模式,所以就會儘可能多的去匹配。 如果傳入字符串1234567,則第一次匹配的位置在12之間,第二次匹配的位置在45之間。 獲得的最終字符串就是1,234,567

如何使用正則表達式

RegExp對象

創建RegExp對象有兩種方式:

  1. 直接字面量的聲明:/d/g
  2. 通過構造函數進行創建:new RegExp('d', 'g')

RegExp對象提供了兩個方法:

exec

方法執行傳入一個字符串,然後對該字符串進行匹配,如果匹配失敗則直接返回null 如果匹配成功則會返回一個數組:

let reg = /([a-z])d+/  let str = 'a233'  let result = reg.exec(str) // => ['a233', 'a', ...]

P.S. 如果正則表達式有g標識,在每次執行完exec後,該正則對象的lastIndex值就會被改變,該值表示下次匹配的開始下標

let reg = /([a-z])d+/g  let str = 'a233'  reg.exec(str) // => ['a233', 'a', ...]  // reg.lastIndex = 4  reg.exec(str) // => null
test

方法用來檢查正則是否能成功匹配該字符串

let reg = /^Hello/    reg.test('Hello World') // => true  reg.test('Say Hello') // => false

test方法一般來說多用在檢索或者過濾的地方。 比如我們做一些篩選filter的操作,用test就是一個很好的選擇。

// 篩選出所有名字為 Niko的數據  let data = [{ name: 'Niko Bellic' }, { name: 'Roman Bellic'}]    data.filter(({name}) => /^Niko/.test(name)) // => [{ name: 'Niko Bellic' }]

String對象

除了RegExp對象實現的一些方法外,String同樣提供了一套方法供大家來使用。

傳入一個正則表達式,並使用該表達式進行匹配; 如果匹配失敗,則會返回-1 如果匹配成功,則會返回匹配開始的下標。 可以理解為是一個正則版的indexOf

'Hi Niko'.search(/Niko/) // => 3  'Hi Niko'.search(/Roman/) // => -1    // 如果傳入的參數為一個字符串,則會將其轉換為`RegExp`對象  'Hello'.search('llo') // => 2
split

split方法應該是比較常用的,用得最多的估計就是[].split(',')了。。

然而這個參數也是可以塞進去一個正則表達式的。

'1,2|3'.split(/,||/) // => [1, 2, 3]    // 比如我們要將一個日期時間字符串進行分割  let date = '2017-11-21 23:40:56'    date.split(/-|s|:/)    // 又或者我們有這麼一個字符串,要將它正確的分割  let arr = '1,2,3,4,[5,6,7]'    arr.split(',') // => ["1", "2", "3", "4", "[5", "6", "7]"] 這個結果肯定是不對的。    // 所以我們可以這麼寫  arr.split(/,(?![,d]+])/) // => ["1", "2", "3", "4", "[5,6,7]"]

該條規則會匹配,,但是,後邊還有一個限定條件,那就是絕對不能出現數字+,的組合併且以一個]結尾。 這樣就會使[4,5,6]裡邊的,不被匹配到。

match

match方法用來檢索字符串,並返回匹配的結果。

如果正則沒有添加g標識的話,返回值與exec類似。 但是如果添加了g標識,則會返回一個數組,數組的item為滿足匹配條件的子串。 這將會無視掉所有的捕獲組。 拿上邊的那個解析HTML來說

let html = '<p><span>text1</span><span>text2</span></p>'    html.match(/<span>(.+?)</span>/g) // => ["<span>text1</span>", "<span>text2</span>"]
replace

replace應該是與正則有關的應用最多的一個函數。 最簡單的模版引擎可以基於replace來做。 日期格式轉換也可以通過replace來做。 甚至match的功能也可以通過replace來實現(雖說代碼會看起來很醜)

replace接收兩個參數 replace(str|regexp, newStr|callback)

第一個參數可以是一個字符串 也可以是一個正則表達式,轉換規則同上幾個方法。 第二個參數卻是可以傳入一個字符串,也可以傳入一個回調函數。

當傳入字符串時,會將正則所匹配到的字串替換為該字符串。 當傳入回調函數時,則會在匹配到子串時調用該回調,回調函數的返回值會替換被匹配到的子串。

'Hi: Jhon'.replace(/Hi:s(w+)/g, 'Hi: $1 Snow') // => Hi: Jhon Snow    'price: 1'.replace(/price:s(d)/g, (/* 匹配的完整串 */str, /* 捕獲組 */ $1) => `price: ${$1 *= 10}`) // => price: 10

一些全新的特性

前段時間看了下ECMAScript 2018的一些草案,發現有些Stage 3的草案,其中有提到RegExp相關的,並在chrome上試驗了一下,發現已經可以使用了。

Lookbehind assertions(應該可以叫做回溯引用吧)

同樣也是一個非捕獲組的語法定義

語法定義:

let reg = /(?<=Pre)w/    reg.test('Prefixer') // => true  reg.test('Prfixer') // => false

設置匹配串前邊必須滿足的一些條件,與(?=)正好相反,一前一後。 這個結合著(?=)使用簡直是神器,還是說解析HTML的那個問題。 現在有了(?<=)以後,我們甚至可以直接通過一個match函數拿到HTML元素中的文本值了。

let html = '<p><span>text1</span><span>text2</span></p>'    html.match(/(?<=<span>)(.+?)(?=</span>)/g) // => ["text1", "text2"]

Named capture groups(命名捕獲組)

我們知道,()標識這一個捕獲組,然後用的時候就是通過1或者$1來使用。 這次草案中提到的命名捕獲組,就是可以讓你對()進行命名,在使用時候可以用接近變量的用法來調用。

語法定義:

let reg = /(?<year>d{4})-(?<month>d{2})-(?<day>d{2})/    '2017-11-21'.match(reg)

match的返回值中,我們會找到一個groupskey。 裡邊存儲着所有的命名捕獲組。

在replace中的用法
let reg = /(?<year>d{4})-(?<month>d{2})-(?<day>d{2})/  '2017-11-21'.replace(reg, '$<month>/$<day>/$<year>') // => 21/11/2017
表達式中的反向引用
let reg = /d{4}-(?<month>d{2})-k<month>/  reg.test('2017-11-11') // => true  reg.test('2017-11-21') // => false

參考資料