# JS正则表达式语法详解
## 前言
正则表达式在线图形化描述:https://regexper.com
什么是正则表达式?说白了,就是用来按照某种规则去匹配字符串的表达式
## RegExp对象
JS 通过内置对象 `RegExp` 支持正则表达式,有两种方法实例化 `RegExp` 对象
- 字面量
- 构造函数
## 字面量
范例:
```javascript
var reg = /\bis\b/
'He is a boy. This is a dog. Where is she?'.replace(reg, 'IS')
// 返回:'He IS a boy. This is a dog. Where is she?'
var reg = /\bis\b/g
'He is a boy. This is a dog. Where is she?'.replace(reg, 'IS')
// 返回:'He IS a boy. This IS a dog. Where IS she?'
```
补充说明:`\b` 是单词边界的意思
## 构造函数
范例:
```javascript
var reg = new RegExp('\\bis\\b')
'He is a boy. This is a dog. Where is she?'.replace(reg, 'IS')
// 返回:'He IS a boy. This is a dog. Where is she?'
var reg = new RegExp('\\bis\\b', 'g')
'He is a boy. This is a dog. Where is she?'.replace(reg, 'IS')
// 返回:'He IS a boy. This IS a dog. Where IS she?'
```
补充说明:
1. 第一个参数是正则表达式的源文本,第二个参数是修饰符
2. `\\bis\\b`,这里为什么要用两个反斜线?因为要当做普通的反斜线文本传递进去,所以需要转义
## 修饰符
- `g`:global 全文搜索,不添加,搜索到第一个匹配停止
- `i`:ignore case 忽略大小写,默认大小写敏感
- `m`:multiple lines 多行搜索
全文搜索和忽略大小写范例:
```javascript
'He is a boy. This is a dog. Is he?'.replace(/\bis\b/, '0')
// 返回:'He 0 a boy. This is a dog. Is he?'
'He is a boy. This is a dog. Is he?'.replace(/\bis\b/g, '0')
// 返回:'He 0 a boy. This 0 a dog. Is he?'
'He is a boy. This is a dog. Is he?'.replace(/\bis\b/gi, '0')
// 返回:'He 0 a boy. This 0 a dog. 0 he?'
```
多行搜索范例:
```javascript
'@123\n@456\n@789'.replace(/^@\d/g, 'X')
// 返回:`X123\n@456\n@789`
'@123\n@456\n@789'.replace(/^@\d/gm, 'X')
// 返回:`X123\nX456\nX789`
```
补充说明:`\n` 是换行转义字符
## 元字符
正则表达式由两种基本字符类型组成:
- 原义字符
- 元字符,元字符是在正则表达式中有特殊含义的非字母字符,如:`* + ? ^ $ . - | \ ( ) { } [ ] \t \v \n \r \f`
## 字符类
一般情况下正则表达式一个字符对应字符串一个字符
我们可以使用元字符 `[]` 来构建一个简单的类
所谓类就是一个泛指,而不是特指某个字符
表达式 `[abc]` 把字符 `a` 或 `b` 或 `c` 归为一类,可以匹配这三个字符
范例:
```javascript
'a1b2c3d4'.replace(/[abc]/g, 'X')
// 返回:'X1X2X3d4'
```
## 字符类取反
使用元字符 `^` 创建反向类
反向类的意思是不属于某类的内容
表达式 `[^abc]` 表示不是字符 `a` 或 `b` 或 `c` 的内容
范例:
```javascript
'a1b2c3d4'.replace(/[^abc]/g, 'X')
// 返回:'aXbXcXXX'
```
## 范围类
正则表达式还提供了范围类
我们可以使用 `[a-z]` 来连接两个字符表示从 `a` 到 `z` 的任意字符
这是个闭区间,也就是包含 `a` 和 `z` 本身
在 `[]` 组成的类内部是可以连写的,如:`[a-zA-Z]`
范例1:
```javascript
'a1b2d3x4z9'.replace(/[a-z]/g, 'Q')
// 返回:'Q1Q2Q3Q4Q9'
```
范例2:
```javascript
'a1b2d3x4z9AJHGYXG'.replace(/[a-zA-Z]/g, 'Q')
// 返回:'Q1Q2Q3Q4Q9QQQQQQQ'
```
范例3:
```javascript
'2019-12-15'.replace(/[0-9]/g, 'X')
// 返回:'XXXX-XX-XX'
'2019-12-15'.replace(/[0-9-]/g, 'X')
// 返回:'XXXXXXXXXX'
```
补充说明:
1. `-` 在 `[]` 里面是指某个范围区间的元字符,若想要把 `-` 也匹配到,就需要转义 `\-`
2. 若 `-` 出现在字符类 `[]` 中,并且 `-` 左右两边都有字符,则为范围区间元字符,若任意一边无字符,则为原义字符 `-`
## 预定义字符类
正则表达式提供预定义字符类来匹配常见的字符类:
- `.`,除了回车符和换行符之外的所有字符,等价于 `[^\r\n]`
- `\d`,数字字符,等价于 `[0-9]`
- `\D`,非数字字符,等价于 `[^0-9]`
- `\s`,空白符,等价于 `[\t\v\r\n\f]`
- `\S`,非空白符,等价于 `[^\t\v\r\n\f]`
- `\w`,单词字符(字符、数字、下划线),等价于 `[a-zA-Z_0-9]`
- `\W`,非单词字符,等价于 `[^a-zA-Z_0-9]`
范例,匹配一个 `ab + 数字 + 单词字符` 的字符串:`/ab\d\w/`
## 边界
正则表达式还提供了几个常用的边界匹配字符
- `^`,以...开始
- `$`,以...结束
- `\b`,单词边界
- `\B`,非单词边界
## 量词
如果我们希望匹配一个连续出现**20次**数字的字符串:
`/\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d\d/`
这样写,未免太 lowb 了
正则表达式提供了量词:
- `?`,出现 0 次或 1 次
- `+`,出现 1 次或多次
- `*`,出现任意次
- `{n}`,出现 n 次
- `{n,m}`,出现 n 到 m 次
- `{n,}`,至少出现 n 次,注意 n 后面的逗号不能省
范例1:`\d{20}`
范例2:`\d{20}\w\d?\w+\d*\d{3}\w{3,5}\d{3,}`
## 贪婪模式
所谓贪婪模式,就是尽可能多的匹配,也就是说就算匹配成功还要继续往下尝试匹配更多满足条件的字符
正则表达式,默认就是贪婪模式
范例:
```javascript
'12345678'.replace(/\d{3,6}/, 'X')
// 返回:'X78'
```
## 非贪婪模式
所谓非贪婪模式,就是让正则表达式尽可能少的匹配,也就是说一旦成功匹配,本次便不再继续尝试匹配更多满足条件的字符
做法很简单,在量词后加上 `?` 即可
范例:
```javascript
'12345678'.replace(/\d{3,6}?/, 'X')
// 返回:'X45678'
```
## 分组
假设我们要匹配字符串 `Byron` 连续出现 3 次的场景,如果这样写:`/Byron{3}/`,是达不到我们想要的效果的,这里实际是 `n` 重复 3 次的意思
使用 `()` 可以达到分组的功能,让量词作用于分组,如:`/(Byron){3}/`,这样就可以满足我们的需求
范例:
```javascript
'a1b2c3d4'.replace(/[a-z]\d{3}/g, 'X')
// 返回:'a1b2c3d4'
'a1b2c3d4'.replace(/([a-z]\d){3}/g, 'X')
// 返回:'Xd4'
```
## 或
正则表达式中,使用 `|` 可以达到或的效果
范例:
```javascript
'ByronCasper'.replace(/Byron|Casper/g, 'X')
// 返回:'XX'
'ByronsperByrCasper'.replace(/Byr(on|Ca)sper/g, 'X')
// 返回:'XX'
```
## 反向引用分组
假设我们想要把 `2019-12-15` 改成 `12/15/2019`,就可以用反向引用分组轻松搞定
范例:
```javascript
'2019-12-15'.replace(/(\d{4})-(\d{2})-(\d{2})/g, '$2/$3/$1')
// 返回:'12/15/2019'
```
补充说明:其中 `$1`、`$2`、`$3`,表示直接引用第几个分组(也就是小括号)匹配到的字符
## 忽略分组
如果不希望捕获某些分组,只需要在分组内加上 `?:` 就可以了
范例(忽略年份这个分组):
```javascript
'2019-12-15'.replace(/(?:\d{4})-(\d{2})-(\d{2})/g, '$2/$3/$1')
// 返回:'15/$3/12'
```
## 前瞻后瞻
正则表达式从文本头部向尾部开始解析,文本尾部方向,称为 **前**
前瞻就是在正则表达式匹配到规则的时候,向前检查是否符合断言,后瞻方向相反
符合和不符合特定断言称为 **肯定** 匹配和 **否定** 匹配
ES9才支持后瞻
语法如下:
- 肯定前瞻,`exp(?=assert)`
- 否定前瞻,`exp(?!assert)`
- 肯定后瞻,`exp(?<=assert)`,ES9才支持
- 否定后瞻,`exp(?
范例:
```javascript
'a2*3'.replace(/\w(?=\d)/g, 'X')
// 返回:'X2*3'
'a2*34v8'.replace(/\w(?=\d)/g, 'X')
// 返回:'X2*X4X8'
'a2*34vv'.replace(/\w(?=\d)/g, 'X')
// 返回:'X2*X4vv'
'a2*34vv'.replace(/\w(?!\d)/g, 'X')
// 返回:'aX*3XXX'
```
## 字符串方法
### String.prototype.search
`search` 方法用于检索字符串中指定的子字符串,或检索与正则表达式匹配的子字符串
方法返回第一个匹配结果的索引,匹配不到返回 **-1**
`search` 方法不执行全局匹配,它将忽略修饰符 `g`,并且总是从字符串的开始进行检索
范例:
```javascript
'a1b2c3d1'.search('1')
// 返回:1
'a1b2c3d1'.search('10')
// 返回:-1
'a1b2c3d1'.search(1)
// 返回:1
'a1b2c3d1'.search(/1/)
// 返回:1
'a1b2c3d1'.search(/1/g)
// 返回:1
```
### String.prototype.match(reg)
`match` 方法将检索字符串,以找到一个或多个与 `reg` 匹配的文本
`reg` 是否具有修饰符 `g`,对结果影响很大
- 没有修饰符 `g`:
- 如果 `reg` 没有修饰符 `g`,那么 `match` 方法就只能在字符串中执行一次匹配
- 如果没有找到任何匹配的文本,将返回 `null`,否则它将返回一个数组,其中存放了与它找到的匹配文本有关的信息
- 返回数组的第一个元素存放的是匹配文本,而其余的元素存放的是与正则表达式的分组匹配的文本
- 除了常规的数组元素之外,返回的数组对象还包含两个额外的属性
- `index`:匹配文本的起始字符在字符串中的位置
- input:源字符串的引用
- 范例:
```javascript
var reg = /\d(\w)\d/
var str = '$1a2b3c4d5e'
var result = str.match(reg)
console.log(result)
// 输出:['1a2', 'a']
console.log(result.index, ',', result.input)
// 输出:1 , $1a2b3c4d5e
```
- 有修饰符 `g`:
- 如果 `reg` 具有修饰符 `g`,则 `match` 方法执行全文检索,找到字符串中所有的匹配文本
- 如果没有找到任何匹配的文本,则返回 `null`,如果找到了一个或多个匹配的文本,则返回一个数组
- 数组元素中存放的是字符串中所有的匹配文本,并且该数组对象没有 `index` 和 `input` 两个额外的属性
- 范例:
```javascript
var reg = /\d(\w)\d/g
var str = '$1a2b3c4d5e'
var result = str.match(reg)
console.log(result)
// 输出:['1a2', '3c4']
console.log(result.index, ',', result.input)
// 输出:undefined , undefined
```
### String.prototype.split
我们经常使用 `split` 方法把字符串分割为字符串数组,如:
```javascript
'a,b,c,d'.split(',')
// 返回:['a', 'b', 'c', 'd']
```
但是在一些复杂的分割情况下,我们就需要使用正则表达式解决,如:
```javascript
'a1b2c3d'.split(/\d/)
// 返回:['a', 'b', 'c', 'd']
```
### String.prototype.replace
我们知道 `replace` 方法是用来替换字符串的,但其实它可以用正则表达式来替换
范例:
```javascript
'a1b'.replace('1', 2)
// 返回:a2b
'a1b1c1'.replace('1', 2)
// 返回:a2b1c1
'a1b1c1'.replace(/1/g, 2)
// 返回:a2b2c2
'a1b1c1'.replace(/1/, 2)
// 返回:a2b1c1
```
补充说明:
- 第二个参数也可以接收一个函数,函数的参数列表如下:
- 匹配的字符串
- 正则表达式分组内容,没有分组则没有该参数
- 匹配项在字符串中的索引
- 源字符串
- 下面用正则表达式有无分组来分别演示:
- 无分组范例:
```javascript
'a1b2c3d4e5'.replace(/\d/g, function (matchedStr, index, origin) {
console.log(index, ',', origin)
return parseInt(matchedStr) + 1
})
// replace后,最终返回:a2b3c4d5e6
// 输出如下:
// 1 , a1b2c3d4e5
// 3 , a1b2c3d4e5
// 5 , a1b2c3d4e5
// 7 , a1b2c3d4e5
// 9 , a1b2c3d4e5
```
- 有分组范例:
```javascript
'a1b2c3d4e5'.replace(/(\d)(\w)(\d)/g, function (matchedStr, group1Str, group2Str, group3Str, index, origin) {
console.log(matchedStr, ',', group1Str, ',', group2Str, ',', group3Str, ',', index, ',', origin)
return group1Str + group3Str
})
// replace后,最终返回:a12c34e5
// 输出如下:
// 1b2 , 1 , b , 2 , 1 , a1b2c3d4e5
// 3d4 , 3 , d , 4 , 5 , a1b2c3d4e5
```