什麼是正則表達式 正則表達式是用於匹配字元串中字元組合的模式。在 JavaScript中,正則表達式也是對象。這些模式被用於 RegExp 的 exec 和 test 方法, 以及 String 的 match、replace、search 和 split 方法。 正則表達式存在於大部分的編程語言, ...
什麼是正則表達式
正則表達式是用於匹配字元串中字元組合的模式。在 JavaScript中,正則表達式也是對象。
這些模式被用於RegExp
的exec
和test
方法, 以及String
的match
、replace
、search
和split
方法。
正則表達式存在於大部分的編程語言,就算是在寫shell
時也會不經意的用到正則。
比如大家最喜歡的rm -rf ./*
,這裡邊的*
就是正則的通配符,匹配任意字元。
在JavaScript
也有正則表達式的實現,差不多就長這個樣子:/\d/
(匹配一個數字)。
個人認為正則所用到的地方還是很多的,比如模版字元的替換、解析URL
,表單驗證 等等一系列。
如果在Node.js
中用處就更為多,比如請求頭的解析、文件內容的批量替換以及寫爬蟲時候一定會遇到的解析HTML
標簽。
正則表達式在JavaScript中的實現
JavaScript中的語法
贅述那些特殊字元的作用並沒有什麼意義,浪費時間。
推薦MDN的文檔:基礎的正則表達式特殊字元
關於正則表達式,個人認為以下幾個比較重要:
貪婪模式與非貪婪模式
P.S. 關於貪婪模式
和非貪婪模式
,發現有些地方會拿這樣的例子:
1 /.+/ // 貪婪模式 2 /.+?/ // 非貪婪模式
僅僅拿這樣簡單的例子來說的話,有點兒扯淡
1 // 假設有這樣的一個字元串 2 let html = '<p><span>text1</span><span>text2</span></p>' 3 4 // 現在我們要取出第一個`span`中的文本,於是我們寫了這樣的正則 5 html.match(/<span>(.+)<\/span>/) 6 // 卻發現匹配到的竟然是 text1</span><span>text2 7 // 這是因為 我們括弧中寫的是 `(.+)` .為匹配任意字元, +則表示匹配一次以上。 8 // 當規則匹配到了`text1`的時候,還會繼續查找下一個,發現`<`也命中了`.`這個規則 9 // 於是就持續的往後找,知道找到最後一個span,結束本次匹配。 10 11 // 但是當我們把正則修改成這樣以後: 12 html.match(/<span>(.+?)<\/span>/) 13 // 這次就能匹配到我們想要的結果了 14 // `?`的作為是,匹配`0~1`次規則 15 // 但是如果跟在`*`、`+`之類的表示數量的特殊字元後,含義就會變為匹配儘量少的字元。 16 // 當正則匹配到了text1後,判斷後邊的</span>命中了規則,就直接返回結果,不會往後繼續匹配。
簡單來說就是:
- 貪婪模式,能拿多少拿多少
- 非貪婪模式,能拿多少拿多少
捕獲組
/123(\d+)0/
括弧中的被稱之為捕獲組。
捕獲組有很多的作用,比如處理一些日期格式的轉換。
1 let date = '2017-11-21' 2 3 date.replace(/^(\d{4})-(\d{2})-(\d{2})$/, '$2/$3/$1')
又或者可以直接寫在正則表達式中作為前邊重覆項的匹配。
1 let template = 'hello helloworl' 2 template.match(/(\w+) \1/) // => hello hello 3 4 // 我們可以用它來匹配出month和day數字相同的數據 5 let dateList = ` 6 2017-10-10 7 2017-11-12 8 2017-12-12 9 ` 10 11 dateList.match(/^\d{4}-(\d{2})-(\1)/gm) // => ["2017-10-10", "2017-12-12"]
非捕獲組
我們讀取了一個文本文件,裡邊是一個名單列表
我們想要取出所有Stark
的名字(但是並不想要姓氏,因為都叫Stark),我們就可以寫這樣的正則:
1 let nameList = ` 2 Brandon Stark 3 Sansa Stark 4 John Snow 5 ` 6 7 nameList.match(/^\w+(?=\s?Stark)/gm) // => ["Brandon", "Sansa"]
上邊的(?=)
就是非捕獲組,意思就是規則會被命中,但是在結果中不會包含它。
比如我們想實現一個比較常用的功能,給數組添加千分位:
1 function numberWithCommas (x = 0) { 2 return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') 3 } 4 5 numberWithCommas(123) // => 123 6 numberWithCommas(1234) // => 1,234
\B
代表匹配一個非單詞邊界,也就是說,實際他並不會替換掉任何的元素。
其次,後邊的非捕獲組這麼定義:存在三的倍數個數字(3、6、9),並且這些數字後邊沒有再跟著其他的數字。
因為在非捕獲組中使用的是(\d{3})+
,貪婪模式,所以就會儘可能多的去匹配。
如果傳入字元串1234567
,則第一次匹配的位置在1
和2
之間,第二次匹配的位置在4
和5
之間。
獲得的最終字元串就是1,234,567
如何使用正則表達式
RegExp對象
創建RegExp
對象有兩種方式:
- 直接字面量的聲明:
/\d/g
- 通過構造函數進行創建:
new RegExp('\d', 'g')
RegExp
對象提供了兩個方法:
exec
方法執行傳入一個字元串,然後對該字元串進行匹配,如果匹配失敗則直接返回null
如果匹配成功則會返回一個數組:
1 let reg = /([a-z])\d+/ 2 let str = 'a233' 3 let result = reg.exec(str) // => ['a233', 'a', ...]
P.S. 如果正則表達式有g
標識,在每次執行完exec
後,該正則對象的lastIndex
值就會被改變,該值表示下次匹配的開始下標
1 let reg = /([a-z])\d+/g 2 let str = 'a233' 3 reg.exec(str) // => ['a233', 'a', ...] 4 // reg.lastIndex = 4 5 reg.exec(str) // => null
test
方法用來檢查正則是否能成功匹配該字元串
1 let reg = /^Hello/ 2 3 reg.test('Hello World') // => true 4 reg.test('Say Hello') // => false
test
方法一般來說多用在檢索或者過濾的地方。
比如我們做一些篩選filter
的操作,用test
就是一個很好的選擇。
1 // 篩選出所有名字為 Niko的數據 2 let data = [{ name: 'Niko Bellic' }, { name: 'Roman Bellic'}] 3 4 data.filter(({name}) => /^Niko/.test(name)) // => [{ name: 'Niko Bellic' }]
String對象
除了
RegExp
對象實現的一些方法外,String
同樣提供了一套方法供大家來使用。
search
傳入一個正則表達式,並使用該表達式進行匹配;
如果匹配失敗,則會返回-1
如果匹配成功,則會返回匹配開始的下標。
可以理解為是一個正則版的indexOf
1 'Hi Niko'.search(/Niko/) // => 3 2 'Hi Niko'.search(/Roman/) // => -1 3 4 // 如果傳入的參數為一個字元串,則會將其轉換為`RegExp`對象 5 'Hello'.search('llo') // => 2
split
split
方法應該是比較常用的,用得最多的估計就是[].split(',')
了。。
然而這個參數也是可以塞進去一個正則表達式的。
1 '1,2|3'.split(/,|\|/) // => [1, 2, 3] 2 3 // 比如我們要將一個日期時間字元串進行分割 4 let date = '2017-11-21 23:40:56' 5 6 date.split(/-|\s|:/) 7 8 // 又或者我們有這麼一個字元串,要將它正確的分割 9 let arr = '1,2,3,4,[5,6,7]' 10 11 arr.split(',') // => ["1", "2", "3", "4", "[5", "6", "7]"] 這個結果肯定是不對的。 12 13 // 所以我們可以這麼寫 14 arr.split(/,(?![,\d]+])/) // => ["1", "2", "3", "4", "[5,6,7]"]
該條規則會匹配,
,但是,
後邊還有一個限定條件,那就是絕對不能出現數字+,
的組合併且以一個]
結尾。
這樣就會使[4,5,6]
裡邊的,
不被匹配到。
match
match
方法用來檢索字元串,並返回匹配的結果。
如果正則沒有添加g
標識的話,返回值與exec
類似。
但是如果添加了g
標識,則會返回一個數組,數組的item
為滿足匹配條件的子串。
這將會無視掉所有的捕獲組。
拿上邊的那個解析HTML
來說
1 let html = '<p><span>text1</span><span>text2</span></p>' 2 3 html.match(/<span>(.+?)<\/span>/g) // => ["<span>text1</span>", "<span>text2</span>"]
replace
replace
應該是與正則有關的應用最多的一個函數。
最簡單的模版引擎可以基於replace
來做。
日期格式轉換也可以通過replace
來做。
甚至match
的功能也可以通過replace
來實現(雖說代碼會看起來很醜)
replace
接收兩個參數replace(str|regexp, newStr|callback)
第一個參數可以是一個字元串 也可以是一個正則表達式,轉換規則同上幾個方法。
第二個參數卻是可以傳入一個字元串,也可以傳入一個回調函數。
當傳入字元串時,會將正則所匹配到的字串替換為該字元串。
當傳入回調函數時,則會在匹配到子串時調用該回調,回調函數的返回值會替換被匹配到的子串。
1 'Hi: Jhon'.replace(/Hi:\s(\w+)/g, 'Hi: $1 Snow') // => Hi: Jhon Snow 2 3 'price: 1'.replace(/price:\s(\d)/g, (/* 匹配的完整串 */str, /* 捕獲組 */ $1) => `price: ${$1 *= 10}`) // => price: 10
一些全新的特性
前段時間看了下
ECMAScript 2018
的一些草案,發現有些Stage 3
的草案,其中有提到RegExp
相關的,併在chrome
上試驗了一下,發現已經可以使用了。
Lookbehind assertions(應該可以叫做回溯引用
吧)
同樣也是一個非捕獲組的語法定義
語法定義:
1 let reg = /(?<=Pre)\w/ 2 3 reg.test('Prefixer') // => true 4 reg.test('Prfixer') // => false
設置匹配串前邊必須滿足的一些條件,與(?=)
正好相反,一前一後。
這個結合著(?=)
使用簡直是神器,還是說解析HTML
的那個問題。
現在有了(?<=)
以後,我們甚至可以直接通過一個match
函數拿到HTML
元素中的文本值了。
1 let html = '<p><span>text1</span><span>text2</span></p>' 2 3 html.match(/(?<=<span>)(.+?)(?=<\/span>)/g) // => ["text1", "text2"]
Named capture groups(命名捕獲組)
我們知道,()
標識這一個捕獲組,然後用的時候就是通過\1
或者$1
來使用。
這次草案中提到的命名捕獲組,就是可以讓你對()
進行命名,在使用時候可以用接近變數的用法來調用。
語法定義:
1 let reg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/ 2 3 '2017-11-21'.match(reg)
在match
的返回值中,我們會找到一個groups
的key
。
裡邊存儲著所有的命名捕獲組。
在replace中的用法
1 let reg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/ 2 '2017-11-21'.replace(reg, '$<month>/$<day>/$<year>') // => 21/11/2017
表達式中的反向引用
1 let reg = /\d{4}-(?<month>\d{2})-\k<month>/ 2 reg.test('2017-11-11') // => true 3 reg.test('2017-11-21') // => false