>[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"]
```
# 位置匹配
位置(锚)是相邻字符之间的位置。比如,下图中箭头所指的地方
![](https://box.kancloud.cn/feca1993f09361ede170e2019aaeb6b2_701x159.png =400x)
如何匹配位置?在 ES5 中,共有 6 个锚: `^、$、\b、\B、(?=p)、(?!p)`
相应的可视化形式是:
![](https://box.kancloud.cn/54318cbe637d2a72630d094f9562d3c4_937x139.png )
## ^ 和 $
^(脱字符)匹配开头,在多行匹配中匹配行开头。
$(美元符号)匹配结尾,在多行匹配中匹配行结尾。
比如我们把字符串的开头和结尾用 "#" 替换:
```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 基础
- 样式设计
- 组件测试
- 图标解决方案
- Algorithm
- 排序算法及常见问题
- 剑指 offer
- 动态规划
- DataStruct
- 概述
- 树
- 链表
- Network
- Performance
- Webpack
- PWA
- Browser
- Safety
- 微信小程序
- mpvue 课程实战记录
- 服务器
- 操作系统基础知识
- Linux
- Nginx
- redis
- node.js
- 基础及原生模块
- express框架
- node.js操作数据库
- 《深入浅出 node.js》笔记
- 前半部分
- 后半部分
- 数据库
- SQL
- 面试题收集
- 智力题
- 面试题精选1
- 面试题精选2
- 问答篇
- Other
- markdown 书写
- Git
- LaTex 常用命令