>[info] 部分內容摘自老姚的《JavaScript 正則表達式迷你書》,[鏈接](https://github.com/qdlaoyao/js-regex-mini-book)。
[TOC]
# 貪婪匹配與惰性匹配
什么是正則表達式的貪婪與惰性匹配?來看下面這段代碼:
```js
let str = "abcaxc"
p1 = /ab.*c/
p2 = /ab.*?c/
console.log(p1.exec(str)) // [ 'abcaxc', index: 0, input: 'abcaxc', groups: undefined ]
console.log(p2.exec(str)) // [ 'abc', index: 0, input: 'abcaxc', groups: undefined ]
```
貪婪匹配:正則表達式一般趨向于最大長度匹配,也就是所謂的貪婪匹配。如上面使用模式 p1 匹配字符串 str,結果就是匹配到:`abcaxc`
惰性匹配:就是匹配到結果就好,盡可能少地匹配。如上面使用模式 p2 (p1 的惰性模式)匹配字符串 str,結果就是匹配到:`abc`
通過在量詞后面加個問號就能實現惰性匹配,比如下面這樣:
| 貪婪量詞 | 惰性量詞 |
| --- | --- |
| {m, n} | {m, n}? |
| {m, } | {m, }? |
| ? | ?? |
| + | +? |
```js
var regex = /\d{2,5}/g
var string = "123 1234 12345 123456"
console.log( string.match(regex) )
// => ["123", "1234", "12345", "12345"]
```
``` js
var regex = /\d{2,5}?/g
var string = "123 1234 12345 123456"
console.log( string.match(regex) )
// => ["12", "12", "34", "12", "34", "12", "34", "56"]
```
# 位置匹配
位置(錨)是相鄰字符之間的位置。比如,下圖中箭頭所指的地方

如何匹配位置?在 ES5 中,共有 6 個錨: `^、$、\b、\B、(?=p)、(?!p)`
相應的可視化形式是:

## ^ 和 $
^(脫字符)匹配開頭,在多行匹配中匹配行開頭。
$(美元符號)匹配結尾,在多行匹配中匹配行結尾。
比如我們把字符串的開頭和結尾用 "#" 替換:
```js
var result = "hello".replace(/^|$/g, '#');
console.log(result);
// => "#hello#"
```
多行匹配模式(即有修飾符 m)時,二者是行的概念,這一點需要我們注意
```js
var result = "I\nlove\njavascript".replace(/^|$/gm, '#');
console.log(result);
/*
#I#
#love#
#javascript#
*/
```
## \\b 和 \\B
`\b`是單詞邊界,具體就是`\w`與`\W`之間的位置,也包括`\w`與`^`之間的位置,和`\w`與`$`之間的位置。 比如考察文件名`"[JS] Lesson\01.mp4"`中的`\b`,如下:
```js
var result = "[JS] Lesson_01.mp4".replace(/\b/g, '#');
console.log(result);
// => "[#JS#] #Lesson_01#.#mp4#"
```
`\B`就是`\b`的反面的意思,非單詞邊界。例如在字符串中所有位置中,扣掉`\b`,剩下的都是`\B`的
```js
var result = "[JS] Lesson_01.mp4".replace(/\B/g, '#');
console.log(result);
// => "#[J#S]# L#e#s#s#o#n#_#0#1.m#p#4"
```
## (?=p) 和 (?!p)
`(?=p)`,其中 p 是一個子模式,即 p 前面的位置,或者說,該位置后面的字符要匹配 p。 比如`(?=l)`,表示 "l" 字符前面的位置,例如:
```js
var result = "hello".replace(/(?=l)/g, '#');
console.log(result);
// => "he#l#lo"
```
而`(?!p)`就是`(?=p)`的反面意思,即該位置的后面不匹配 p:
```js
var result = "hello".replace(/(?!l)/g, '#');
console.log(result);
// => "#h#ell#o#"
```
## 例題
千分符表示法一個常見的應用就是貨幣格式化。 比如把下面的字符串:
`1888`格式化為`$ 1,888.00`
```js
function format (num) {
return num.toFixed(2).replace(/\B(?=(\d{3})+\b)/g, ",").replace(/^/, "$$ ");
// replace 函數里兩個 $$ 才能表示美元符號,因為第二個參數 $ 有特殊含義
};
console.log(format(1888));
// => "$ 1,888.00"
```
# 正則表達式括號的作用
## 分組
我們知道 `/a+/` 匹配連續出現的 `"a"`,而要匹配連續出現的 `"ab"` 時,需要使用 `/(ab)+/`。
其中括號是提供分組功能,使量詞 `+` 作用于 `"ab"` 這個整體,測試如下:
```js
var regex = /(ab)+/g;
var string = "ababa abbb ababab";
console.log( string.match(regex) );
// => ["abab", "ab", "ababab"]
```
## 分支結構
在多選分支結構`(p1|p2)`中,此處括號的作用也是不言而喻的,提供了分支表達式的所有可能。 比如,要匹配如下的字符串:
```js
I love JavaScript
I love Regular Expression
```
可以使用正則:
```js
var regex = /^I love (JavaScript|Regular Expression)$/;
console.log( regex.test("I love JavaScript") );
console.log( regex.test("I love Regular Expression") );
// => true
// => true
```
如果去掉正則中的括號,即`/^I love JavaScript|Regular Expression$/`
匹配字符串是 "I love JavaScript" 和 "Regular Expression",當然這不是我們想要的。
## 替換
比如,想把 yyyy-mm-dd 格式,替換成 mm/dd/yyyy 怎么做?
```js
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, "$2/$3/$1");
console.log(result);
// => "06/12/2017"
```
其中 replace 中的,第二個參數里用 $1、$2、$3 指代相應的分組
## 反向引用
在正則本身里引用之前的分組,即反向引用,如 \\1 表示第一個分組
比如要寫一個正則支持匹配如下三種格式
```js
2016-06-12
2016/06/12
2016.06.12
```
```js
var regex = /\d{4}(-|\/|\.)\d{2}\1\d{2}/;
var string1 = "2017-06-12";
var string2 = "2017/06/12";
var string3 = "2017.06.12";
var string4 = "2016-06/12";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // true
console.log( regex.test(string4) ); // false
```
# JavaScript 中使用正則
## RegExp
創建正則表達式,兩種寫法,一種是直接`/正則表達式/`,另一種是使用 RegExp 的構造方法 `new RegExp(pattern, attributes)`傳入的參數都為字符串/字符,第二個參數是修飾符 'i'、'g'、'm'
>[danger]應該優先使用字面量形式,因為用構造函數會寫很多 \
```js
var re1 = /ABC\-001/
var re2 = new RegExp('ABC\\-001')
re1 // /ABC\-001/
re2 // /ABC\-001/
```
一個 RegExp 對象有 exec 和 test 方法,比如上面的 re1 和 re2 可以這么使用`re1.test(str)`
- test 方法檢索字符串是否滿足正則匹配。返回 true 或 false。
- exec 方法執行對字符串的正則匹配。返回一個數組包含相關信息(如果不滿足則返回 null)。
```js
let string = "2017.06.27"
let reg = /\b(\d+)\b/g // \b 匹配單詞邊界
let result
while (result = reg.exec(string)) {
console.log(result, reg.lastIndex)
}
// => ["2017", "2017", index: 0, input: "2017.06.27"] 4
// => ["06", "06", index: 5, input: "2017.06.27"] 7
// => ["27", "27", index: 8, input: "2017.06.27"] 10
```
exec 方法返回的數組的第 0 個元素是與正則表達式相匹配的文本,第 1 個元素是與 RegExpObject 的第 1 個分組相匹配的文本(如果有的話),第 2 個元素是與 RegExpObject 的第 2 個分組相匹配的文本(如果有的話),依次類推。
<br/>
除了數組元素和 length 屬性之外,exec() 方法還返回兩個屬性(可以通過res.input 和 res.index 來訪問)。index 屬性聲明的是匹配文本的第一個字符的位置。input 屬性則存放的是被檢索的字符串 string。我們可以看得出,在調用非全局的 RegExp 對象的 exec() 方法時,返回的數組與調用方法 String.match() 返回的數組是相同的。
<br/>
但是,當 RegExpObject 是一個**全局正則表達式**時,exec() 的行為就稍微復雜一些。它會在 RegExpObject 的 lastIndex 屬性指定的字符處開始檢索字符串 string。當 exec() 找到了與表達式相匹配的文本時,在匹配后,它將把 RegExpObject 的 lastIndex 屬性設置為匹配文本的**最后一個字符**的下一個位置。這就是說,您可以通過反復調用 exec() 方法來遍歷字符串中的所有匹配文本。當 exec() 再也找不到匹配的文本時,它將返回 null,并把 lastIndex 屬性重置為 0。
# 字符串中可以使用正則表達式的方法
## match
match() 方法只接受一個參數,要么是一個正則表達式,要么是一個 RegExp 對象
match() 方法將檢索字符串 stringObject,以找到一個或多個與 regexp 匹配的文本。這個方法的行為在很大程度上有賴于 regexp 是否具有標志 g。
<br/>
如果 regexp 沒有標志 g,那么 match() 方法就只能在 stringObject 中執行一次匹配。如果沒有找到任何匹配的文本, match() 將返回 null。否則,它將返回一個數組,其中存放了與它找到的匹配文本有關的信息。該數組的第 0 個元素存放的是匹配文本,而其余的元素存放的是與正則表達式的子表達式匹配的文本。除了這些常規的數組元素之外,返回的數組還含有兩個對象屬性。index 屬性聲明的是匹配文本的起始字符在 stringObject 中的位置,input 屬性聲明的是對 stringObject 的引用。
<br/>
如果 regexp 具有標志 g,則 match() 方法將執行全局檢索,找到 stringObject 中的所有匹配子字符串。若沒有找到任何匹配的子串,則返回 null。如果找到了一個或多個匹配子串,則返回一個數組。不過全局匹配返回的數組的內容與前者大不相同,它的數組元素中存放的是 stringObject 中所有的匹配子串,而且也沒有 index 屬性或 input 屬性。
可以看到,在全局檢索模式下,match() 既不提供與子表達式匹配的文本的信息,也不聲明每個匹配子串的位置。如果需要這些全局檢索的信息,可以使用 RegExp.exec()。
```js
var text = "cat, bat, sat, fat"
var pattern = /.at/
// 與 pattern.exec(text) 相同
var matches = text.match(pattern)
console.log(matches.index) // 0
console.log(matches[0]) // "cat"
console.log(pattern.lastIndex) // 0
console.log(matches) // ['cat', index:0, input:'cat,bat,sat,fat' ]
```
```js
var text = "cat, bat, sat, fat"
var pattern = /.at/g
var matches = text.match(pattern)
console.log(matches.index) // undefined
console.log(matches[0]) // "cat"
console.log(pattern.lastIndex) // 0
console.log(matches) // ['cat','bat','sat','fat']
```
## search
這個方法的唯一參數與 match() 方法的參數相同:由字符串或 RegExp 對象指定的一個正則表達式。search() 方法返回字符串中第一個匹配項的索引;如果沒有找到匹配項,則返回 -1。而且,search() 方法始終是從字符串開頭向后查找模式。
>[warning]似乎 RegExp 對象的 exec 和 test 方法可以完美地取代這兩個字符串方法?
## replace
這個方法接受兩個參數:第一個參數可以是一個 RegExp 對象或者一個字符串(這個字符串不會被轉換成正則表達式),第二個參數可以是一個字符串或者一個函數。如果第一個參數是字符串,那么只會替換第一個子字符串。**要想替換所有子字符串,唯一的辦法就是提供一個正則表達式,而且要指定全局(g)標志**,如下所示
```js
var text = "cat, bat, sat, fat"
var result = text.replace("at", "ond")
console.log(result) // "cond, bat, sat, fat"
result = text.replace(/at/g, "ond")
console.log(result) // "cond, bond, sond, fond"
```
第二個參數,可以是字符串,也可以是函數。
當第二個參數是字符串時,如下的字符有特殊的含義:
| 屬性 |描述|
| --- | --- |
|$1,$2,…,$99 |匹配第 1-99 個分組里捕獲的文本|
|$&| 匹配到的子串文本|
|$`| 匹配到的子串的左邊文本|
|$' |匹配到的子串的右邊文本|
|$$| 美元符號|
例如,把 "2,3,5",變成 "5=2+3":
```js
var result = "2,3,5".replace(/(\d+),(\d+),(\d+)/, "$3=$1+$2")
console.log(result)
// => "5=2+3"
```
當第二個參數是函數時,我們需要注意該回調函數的參數具體是什么:
```js
"1234 2345 3456".replace(/(\d)\d{2}(\d)/g, function (match, $1, $2, index, input) {
console.log([match, $1, $2, index, input])
})
// => ["1234", "1", "4", 0, "1234 2345 3456"]
// => ["2345", "2", "5", 5, "1234 2345 3456"]
// => ["3456", "3", "6", 10, "1234 2345 3456"]
```
在正則表達式中定義了多個捕獲組的情況下,傳遞給函數的參數依次是模式的匹配項、第一個捕獲組的匹配項、第二個捕獲組的匹配項……,最后兩個參數分別是模式的匹配項在字符串中的位置(這個匹配項的第一個字符在字符串中的位置)和原始字符串。**這個函數應該返回一個字符串,表示應該被替換的匹配項。**
```js
function htmlEscape (text) {
return text.replace(/[<>"&]/g, function (match, pos, originalText) {
switch (match) {
case '<':
return '<'
case '>':
return '>'
case '&':
return '&'
case '\"':
return '"'
}
})
}
console.log(htmlEscape(`<p class="greeting">Hello world!</p>`))
// <p class="greeting">Hello world!</p>
```
## split
split 也可以使用正則
```js
var regex = /\D/;
console.log( "2017/06/26".split(regex) );
console.log( "2017.06.26".split(regex) );
console.log( "2017-06-26".split(regex) );
// => ["2017", "06", "26"]
// => ["2017", "06", "26"]
// => ["2017", "06", "26"]
```
# 例題
## 1.使用正則表達式去除字符串中重復的字符
``` js
var str = "aaabbb___cccddd"
str = str.replace(/(.)\1*/g, '$1')
console.log(str) // ab_cd
```
`\1`用于正則表達式內取值,取的是第一個分組匹配到的值。
`$1`用于正則表達式外取值,?取的是第一個分組匹配到的值。
## 2.驗證手機號
``` js
var reg = /^1[3578]\d{9}/
var str = '15616460659'
console.log(reg.test(str)) // true
```
## 3.寫函數實現任意標簽轉換成 json 形式
```javaScript
/*
<div>
<span>
<a></a>
</span>
<span>
<a></a>
<a></a>
</span>
</div>
*/
function DOM2JSON(str) {
let reg = /<(.+)>(.*?)<\/\1>/g // 注意 .\*? 是惰性匹配,如果使用 .\* 這樣的情況會出問題: <span><a></a></span><span></span> 不會最短地閉合
let result = null
let nodes = []
while((result = reg.exec(str)) !== null) { // 當 exec() 再也找不到匹配項后它將返回 null,并把 lastIndex 屬性重置為 0
nodes.push({ tag: result[1], children: DOM2JSON(result[2])}) // exec 返回的數組,[0]匹配的字符串 然后依次是捕獲的分組 然后有 index 和 input 屬性
}
return nodes.length > 1 ? nodes : nodes[0]
}
console.log(JSON.stringify(DOM2JSON('<div><span><a></a></span><span><a></a><a></a></span></div>')))
// {"tag":"div","children":[{"tag":"span","children":{"tag":"a"}},{"tag":"span","children":[{"tag":"a"},{"tag":"a"}]}]}
```
這里主要利用了 exec 函數會在上一次匹配的結果之后繼續匹配,且如果未匹配成功會返回 null,然后注意下 exec 和正則表達式分組的使用即可。
## 4.匹配 16 進制顏色
要求匹配:
```css
#ffbbad
#Fc01DF
#FFF
#ffE
```
分析:
表示一個 16 進制字符,可以用字符組 [0-9a-fA-F]。
其中字符可以出現 3 或 6 次,需要是用量詞和分支結構。
使用分支結構時,需要注意順序。
```js
var regex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g;
var string = "#ffbbad #Fc01DF #FFF #ffE";
console.log( string.match(regex) );
// => ["#ffbbad", "#Fc01DF", "#FFF", "#ffE"]
```
## 5.windows 操作系統文件路徑
要求匹配:
```shell
F:\study\javascript\regex\regular expression.pdf
F:\study\javascript\regex\
F:\study\javascript
F:\
```
分析:
整體模式是:
```shell
盤符:\文件夾\文件夾\文件夾\
```
其中匹配 `"F:\"`,需要使用 `[a-zA-Z]:\\`,其中盤符不區分大小寫,注意 \ 字符需要轉義。
文件名或者文件夾名,不能包含一些特殊字符,此時我們需要排除字符組 `[^\\:*<>|"?\r\n/]`來表示合法字符。
另外它們的名字不能為空名,至少有一個字符,也就是要使用量詞 +。因此匹配 `文件夾\`,可用 `[^\\:*<>|"?\r\n/]+\\`
另外 `文件夾\`,可以出現任意次。也就是 `([^\\:*<>|"?\r\n/]+\\)*`。其中括號表示其內部正則是一個整體。
路徑的最后一部分可以是 文件夾,沒有 \,因此需要添加 `([^\\:*<>|"?\r\n/]+)?`。
```js
var regex = /^[a-zA-Z]:\\([^\\:*<>|"?\r\n/]+\\)*([^\\:*<>|"?\r\n/]+)?$/;
console.log( regex.test("F:\\study\\javascript\\regex\\regular expression.pdf") ); // 在JavaScript 中字符串要表示字符 \\ 時,也需要轉義
console.log( regex.test("F:\\study\\javascript\\regex\\") );
console.log( regex.test("F:\\study\\javascript") );
console.log( regex.test("F:\\") );
// => true
// => true
// => true
// => true
```
# 速查表
## 字符組
| 模式 | 說明 |
| --- | --- |
| [abc] | 匹配 "a"、"b"、"c" 其中任意一個字符 |
| [a-d1-4] | 匹配 "a"、 "b"、 "c"、 "d"、 "1"、 "2"、 "3"、 "4" 其中任意一個字符 |
| [^abc] | 匹配除了 "a"、"b"、"c" 之外的任意一個字符 |
| [^a-d1-4]] | 匹配除了 "a"、 "b"、 "c"、 "d"、"1"、 "2"、 "3"、"4" 之外的任意一個字符 |
| . | 通配符,匹配除了少數字符 (\\n) 之外的任意字符 |
| \d | 匹配數字,等價于 [0-9] |
| \D | 匹配非數字,等價于 [^0-9] |
| \w | 匹配單詞字符,等價于 [a-zA-Z0-9_] |
| \W | 匹配非單詞字符,等價于 [^a-zA-Z0-9_] |
| \s | 匹配空白符,等價于 [\\t \\v \\n \\r \\f] |
| \S | 匹配非空白符,等價于 [^\\t \\v \\n \\r \\f] |
## 量詞
| 模式 | 說明 |
| --- | --- |
| {n, m} | 連續出現 n 到 m 次 |
| {n, } | 至少連續出現 n 次 |
| {n} | 連續出現 n 次 |
| ? | 等價于 {0,1},0 次或 1 次 |
| + | 等價于 {1, } 1 次及以上 |
| * | 等價于 {0, } 0 次及以上 |
## 修飾符
| 符號 | 說明 |
| --- | --- |
| g | 全局匹配,找到所有滿足匹配的子串 |
| i | 匹配過程中,忽略英文字母大小寫 |
| m | 多行匹配,把 ^ 和 $ 變成行開頭和結尾 |
## 元字符轉義
所謂元字符,就是正則中有特殊含義的字符。 所有結構里,用到的元字符總結如下: `^`、`$`、`.`、`*`、`+`、`?`、`|`、`\`、`/`、`(`、`)`、`[`、`]`、`{`、`}`、`=`、`!`、`:`、`-`
當匹配上面的字符本身時,可以一律轉義:
```js
var string = "^$.*+?|\\/[]{}=!:-,";
var regex = /\^\$\.\*\+\?\|\\\/\[\]\{\}\=\!\:\-\,/;
console.log( regex.test(string) );
// => true
```
- 序言 & 更新日志
- H5
- Canvas
- 序言
- Part1-直線、矩形、多邊形
- Part2-曲線圖形
- Part3-線條操作
- Part4-文本操作
- Part5-圖像操作
- Part6-變形操作
- Part7-像素操作
- Part8-漸變與陰影
- Part9-路徑與狀態
- Part10-物理動畫
- Part11-邊界檢測
- Part12-碰撞檢測
- Part13-用戶交互
- Part14-高級動畫
- CSS
- SCSS
- codePen
- 速查表
- 面試題
- 《CSS Secrets》
- SVG
- 移動端適配
- 濾鏡(filter)的使用
- JS
- 基礎概念
- 作用域、作用域鏈、閉包
- this
- 原型與繼承
- 數組、字符串、Map、Set方法整理
- 垃圾回收機制
- DOM
- BOM
- 事件循環
- 嚴格模式
- 正則表達式
- ES6部分
- 設計模式
- AJAX
- 模塊化
- 讀冴羽博客筆記
- 第一部分總結-深入JS系列
- 第二部分總結-專題系列
- 第三部分總結-ES6系列
- 網絡請求中的數據類型
- 事件
- 表單
- 函數式編程
- Tips
- JS-Coding
- Framework
- Vue
- 書寫規范
- 基礎
- vue-router & vuex
- 深入淺出 Vue
- 響應式原理及其他
- new Vue 發生了什么
- 組件化
- 編譯流程
- Vue Router
- Vuex
- 前端路由的簡單實現
- React
- 基礎
- 書寫規范
- Redux & react-router
- immutable.js
- CSS 管理
- React 16新特性-Fiber 與 Hook
- 《深入淺出React和Redux》筆記
- 前半部分
- 后半部分
- react-transition-group
- Vue 與 React 的對比
- 工程化與架構
- Hybird
- React Native
- 新手上路
- 內置組件
- 常用插件
- 問題記錄
- Echarts
- 基礎
- Electron
- 序言
- 配置 Electron 開發環境 & 基礎概念
- React + TypeScript 仿 Antd
- TypeScript 基礎
- React + ts
- 樣式設計
- 組件測試
- 圖標解決方案
- Storybook 的使用
- Input 組件
- 在線 mock server
- 打包與發布
- Algorithm
- 排序算法及常見問題
- 劍指 offer
- 動態規劃
- DataStruct
- 概述
- 樹
- 鏈表
- Network
- Performance
- Webpack
- PWA
- Browser
- Safety
- 微信小程序
- mpvue 課程實戰記錄
- 服務器
- 操作系統基礎知識
- Linux
- Nginx
- redis
- node.js
- 基礎及原生模塊
- express框架
- node.js操作數據庫
- 《深入淺出 node.js》筆記
- 前半部分
- 后半部分
- 數據庫
- SQL
- 面試題收集
- 智力題
- 面試題精選1
- 面試題精選2
- 問答篇
- 2025面試題收集
- Other
- markdown 書寫
- Git
- LaTex 常用命令
- Bugs