这些是本人在 github.pages 上写的博客,欢迎大家关注和纠错,本人会定期在github pages 上更新。有想要深入了解的知识点可以留言。
同时,这是本人第一次写文章,如有目录结构不合理,还请指出。
前言
刚开始学习 JS 时,正则表达式一直是我不愿意面对的,每次读到有关正则表达式的时候,都会避而远之。可是,一次,当我打开 JQ 源码的时候,发现里面有大量的正则表达式。于是乎,自己就强迫自己学习正则,学习的过程还是蛮愉快的。最后,真香定律终于出现了。哈哈哈!!
这篇教程我会由浅入深的来和大家分享正则表达式,让大家即学习到正则表达式的用法,也了解其在 JS 中表现的不为人知的一面。
概述
正则表达式是 JS 中很重要的一环。也是对很多人比较不愿意面对的一个知识点。但是,当我们真正掌握了正则表达式,可以利用其在我们的代码中发挥很大的威力,大大的简化我们的代码。对于喜欢阅读一些库源码的伙伴。这个真的是必须掌握的。
当然,正则基本在每个语言都有实现。虽不能说都相同。但是基本上都是大同小异。此外,正则表达式的范围非常的广,这里也不可能每个知识点都会涉及到。这里,作者会将一些我认为常见的,重要的,常见的注意点给大家一一分析。
正则表达式基础概念
定义
正则表达式,英文叫做 Regular Expression。按照英文字面意思解释,就是有规则的表达式,正则表达式就是由一些列的语法规则组成的字符串,然后按照这种组合的规则去匹配一些字符串,筛选出符合条件的字符串。
知识了解:根据 ECMA5 规范,JS 中正则表达式的格式和功能是以 perl 语言为蓝本的。
我们平时写的正则表达式很不直观,这里推荐一个在线工具。该工具以可视化的界面来描述我们写的正则表达式。工具比较简单,大家自行了解。 在线工具
正则表示方法的区别
正则表达式的表示方法有两种
- 字面量表示法
- 构造函数表示法
let reg = /text/ig
let regExp = new RegExp('text', ig)
两种表示方法都可行。区别在于:利用构造函数进行表示的时候,可以动态的构建正则表达式的规则。
let str = String('****')
// 如 let className = str + 'name'
regExp = new RegExp(className, 'ig')
正则表达式的组合规则太多了,大家可以去看一下 ECMA 规范。下面我们就介绍一些常用的,经常遇到的情况。
正则表达式组成元素
正则表达式的组成一般由以下几类构成
- 原义文本字符
- 元字符
- 修饰词
修饰词
在 JavaScript 中,全局修饰词有 g、i、m、y、u、s
这些修饰词在正则表达式的匹配中,起到了很重要的作用。
g: 代表全局匹配,当匹配到目标字符串的时候,不会停止匹配,而会继续匹配。直到匹配结束为止。
i: 匹配的过程中,忽略大小写
m: 换行匹配。字符串可以换行,如果当前行没有匹配到,可以换行继续匹配
y: 执行“粘性”搜索,匹配从目标字符串的当前位置开始
u: 相当于将匹配模式转换成 unicode 模式,可以正确处理大于\uFFFF的 Unicode 字符。大家可以自行参考 了解
s: 我们知道 元字符 . 代表除了回车换行符以外的所有字符,但是加上 s 修饰符后, . 可以匹配任意字符
/./.text('\n') // false
/./s.test('\n') // true
//其实还有另一种技巧可以匹配所有字符
/[^]/.test('\n') // true
y 代表什么含义? 这个等会再做解释,先简单说下,这个与正则表达式的一个属性 lastlndex 有关系,现在解释,可能会一脸懵逼。 下面会与 g 标志 一起讨论。
元字符
元字符是在正则表达式中有特殊含义的非字母字符。这些元字符使得正则表达式的组合规则十分强大。
// 引用自规范原文 我们可以看一下这些元字符都有哪些,我们应该很熟悉这其中代表的含义。
PatternCharacter :: SourceCharacter but not any of:
^ $ \ . * + ? ( ) [ ] { } |
此外,还有一些字符,是组合而成的,代表特殊的含义
有 \b、\B、\d、\D、\w、\W、\s、\S、\f、\v、\t、\n、\r 等等。这些字符代表的含义,大家自行了解,这里不一一讲解这些元字符的含义。
下面举例子会用到一些,会对其含义做说明。
既然元字符代表这么多的含义,那么我们如果需要在字符串中匹配这些字符怎么办呢? 这个时候,可以使用转义字符帮忙。
举个栗子:
reg = /\d/ // \d 是元字符,表示数字,这个正则表达式只能匹配数字,如果我们需要匹配'\d'呢
reg.test('\d') // false
这时候需要用转义字符转义
reg = /\\d/ // \ 代表转义字符
reg.test('\d') // 还是false
这个为什么还是 false 呢,不是按照规矩办事吗?
这是因为 JS 里的字符串也有转义字符!
可以试一下 '\d'.length 等于1,这个时候要想匹配 '\d' 必须要在字符串中对其转义
reg.test('\\d') // true
总结:遇到这种情况,别总是忙着为正则表达式转义,还得为字符串转义,关于字符串里面的转义字符,这超过了本篇讨论范围,大家自行了解。
常见元字符的解释
下面介绍一些常用的元字符
元字符 | 表示及含义 | 解释 |
---|---|---|
. | /[ ^\r\n ]/ | 除了回车换行以外的全部字符 |
\d | /[0-9]/ | 数字字符 |
\D | /[^0-9]/ | 非数字字符 |
\w | /[0-9a-zA-Z_]/ | 单词字符(数字,字母,下划线) |
\W | /[^0-9a-zA-Z_]/ | 代表非单词字符 |
\s | /[\f\n\r\t\v\u00a0\u1680\u180e \u2000-\u200a\u2028\u2029\u202f \u205f\u3000\ufeff]/ |
空白字符,包括空格、 制表符、换页符和换行符 |
\S | /[^\s]/ | 非空白字符 |
| | /x|y/ | x or y |
\b | word boundary | 单词边界 |
\B | none word boundary | 非单词边界 |
^ | /^abc/ | 匹配以abc为开始的字符串 |
$ | /abc$/ | 匹配以abc为结束的字符串 |
字符类
我们都知道用特定的正则表达式去匹配特定的字符串很轻松。因为不会出现其他情况,逻辑上讲是非常清晰的事情。
举个栗子:
let reg = /abc\b/
// 如下图 表示匹配 abc后面跟上单词边界。
reg.test('abc bcd') // true
reg.test('abcc') //false 因为abc后面紧跟单词边界

可是,有时候我们的需求不是匹配特定字符,而是要匹配符合一些特性的字符。比如,需要匹配 a b c 任意一个,存在即满足条件。
简而言之:我只要你们中的一个出现就OK。
这个时候,我们就可以使用元字符 [] 来构建这样一个 字符类。
这里的类,我们可以联想到编程语言的类,泛指一些符合特性的事物,而不是特指。
举个例子:
let reg = /[abc]\b/
// 如下图 表示 one of abc 后面紧跟单词边界就满足条件
reg.test('a') // true

字符类很强大,但是,如果我们的需求是要匹配除了一个字符类之外的字符呢?
简而言之:别人都行,就你们不可以。
这个时候,我们可以使用字符类的反向类,使用元字符 ^ 在写好的字符类里面取反。
举个例子:
let reg = /[^abc]\b/
// 如下图 表示 None of abc 后面紧跟单词边界就满足条件
reg.test('e') // true

解释一下单词边界的含义:单词边界这个概念,很多人都比较模糊。我只能说一下我的理解 在正则表达式中,\w 代表单词字符,\W 代表非单词字符,只要单词字符紧挨着非单词字符,那么在这两者中间,就存在单词边界。
范围类
字符类给我们注入了一种全新的功能,类似于数据库的模糊查询。我们可以利用这一功能匹配范围内的字符串了。 但是,应用场景多了,也会出现问题。
举个例子:
//我们想要匹配 数字1到8中的任意一个,我们利用字符类的概念可以这样写
reg = /[12345678]/
// 可能有的小伙伴可以接受,那好,如果我们想要匹配小写字母,a 到 z 的任意一个字符
reg = /[abcdefghijklmnopqrstuvwxyz]/ // 这样一坨,写的很难受
看上面的例子,写代码的难受,读代码的估计也不舒服。这时候,我们需要范围类帮忙。
所谓的范围类,就是匹配具有一定规则的一段范围之内的字符:
使用字符 -,来达成这一目标
举个例子:
// 匹配 a 到 z 的任意一个字符
reg = /[a-z]/
// 匹配除了 a 到 z 的任意一个字符
reg = /[^a-z]/
// 常见的模式
/\d/ 就相当于 /[0-9]/
/\D/ 就相当于 /[^0-9]/
/\w/ 就相当于 /[a-zA-Z0-9]/
/\W/ 就相当于 /[^a-zA-Z0-9]/

一个问题:**-**不是元字符,是否可以在范围类中匹配?如果可以,是否需要转义或者其他特殊操作。
匹配该字符在字符类中是可以的,但是有注意点:即 - 只可以放在范围类的开头或结尾,才会匹配该字符。
不可以出现在两个字符中间,不然,该字符还是会被当作范围类中的特殊字符来对待
举个例子:
let reg = /[1-z]/
reg.test('-') // false
reg.test('a') // true
let reg = /[1-9-]/
reg.test('-') // true
reg.test('1') // true

量词
我们之前介绍的字符类或非字符类,只能匹配特定类出现一次,如果出现多次,需要额外再写相同的代码进行匹配。
举个例子:
// 需求:匹配有连续5个数字的字符串
let reg = /\d\d\d\d\d/ // 那如果要匹配出现连续3至6个数字的字符串呢?
let reg = /\d\d\d|\d\d\d\d|\d\d\d\d\d|\d\d\d\d\d\d/ // 这样写太复杂,我需要更简单的写法
有这样的需求时,我们就需要量词来帮忙。
量词有几种表示的方式,各自代表不同的需求。
量词的表示有以下这几种表示:
+ 表示匹配一次或一次以上。one or more
? 表示匹配0次或一次。 one or less
* 表示匹配0次或0次以上。none or onemore
{m,n} 表示匹配 m 到 n 次。[m,n]闭区间 m less n most
{m,} 表示匹配至少 m 次。 m less
{m} 表示匹配出现 m 次
如果要表示至多出现 m 次,可以这样表示 {0,m}
重写例子:
reg = /\d{5}/ // 匹配有5个连续数字的字符串
reg = /\d{3,6}/ // 匹配有连续 3 至 6 个数字的字符串
大家可以在图形化工具里面自己尝试下,很直观。
正则表达式的贪婪模式
注意:这里,我们会先应用 String.prototype.replace 方法来很形象的解释正则表达式的贪婪模式。
来看一下这样等应用场景:我们需要匹配连续 5-10 个小写字母等场景。目标字符串满足这个需求,但是匹配等结果是什么?
是匹配到5个字母就不匹配还是继续匹配更多等字母直到匹配失败呢?
人是贪婪的,所以人设计的正则表达式也是贪婪的。
在正则表达式中,会尽可能的匹配更多的字符,直到匹配失败为止。
举个例子:
reg = /[a-z]{5,10}/
str = 'ahhsjkiosbsasdasllk' // str.length === 19
我们用 replace 方法来验证一下,正则表达式匹配了多少字符
str1 = str.replace(reg, 'Q') // str1.length === 10
str1 = 'Qsasdasllk'
上述的例子,我们可以看出,正则表达式是属于贪婪模式。那么我们现在想要取消贪婪模式,可以吗?
可以,只需要在量词后加上**元字符?**就可以取消贪婪模式啦。
举个例子:
reg = /[a-z]{5,10}?/
str = 'ahhsjkiosbsasdasllk' // str.length === 19
我们用 replace 方法来验证一下,正则表达式匹配了多少字符
str1 = str.replace(reg, 'Q') // str1.length === 15
str1 = 'Qkiosbsasdasllk'
分组
假如,我们现在需要这样一个需求,需要匹配包含'mistyyyy'连续重复2次的字符串。
这种情况,我们按照之前的写法可能会这样写。
/mistyyyy{2}/
但是,这样匹配是错误的,这表达的意思是y重复2次,而不是 mistyyyy 重复2次
这个时候,我们可以使用分组这个概念来帮助我们。
用法:将需要分组的信息,用元字符()包含起来。这样就可以使量词作用于分组了。
reg = /(mistyyyy){2}/
str = 'Im mistyyyymistyyyymistyyyy yeah'
reg.test(str) // true
如下图所示

再看一个例子,这时候,我要改名字了。mistyyyy 或者 missyyyy 都是可以的。那我们怎么匹配它呢?
//我们可以这样写
reg = /mistyyyy|missyyyy/
但是利用分组,我们可以这样写
reg = /mis(s|t)yyyy/
reg.test('mistyyyy') // true
reg.test('missyyyy') // true
捕获和非捕获分组
现在我们来看一个,平时开发中经常出现的需求。如:将日期 '2018-12-23' 转换为 '23/12/2018'
这个时候,我们就很头大了。单纯的匹配到这个日期并不困难。但是如何将其转换这就成了难点。当然,我们可以进行最原始的方法进行解决。
reg = /\d{4}-\d{2}-\d{2}/
'2018-12-23'.replace(reg, '23/12/2018')
// 这样的程序基本没有灵活性。
这时候,我们要讲的捕获就要出现了。前面讲到了分组,既然可以分组,那我们也可以捕获分组。
捕获分组又可以称为引用:
- 一种是正向引用,用于正则表达式里面的表示。
- 另一种称为反向引用,常用于正则表达式匹配结果的替换。
我们先看正向引用,举个最适合的例子。
//我们现在需要匹配一个 dom 节点,比如匹配 id 为 container 的 div dom 节点。
let domContainer =
`<div>
<div id="container">
this is container
</div>
</div>`
reg = /<div id="container">([^<\/]+)<\/div>/m
domContainer.replace(reg, 's') // <div>s</div>
这个时候,我们可以使用正向引用,即 \1 代表第一个分组的引用, \2 代表第二个分组的引用等等 以此类推
我们来重写正则表达式。
reg = /<(div) id="container">([^<\/]+)<\/\1>/m
// 大家可以试一下,同样的效果。
介绍完正向引用,我们来看一下反向引用。
在正则表达式进行分组时,当匹配结束时候,我们希望可以以分组为单位进行字符串的替换,这样可行吗?
举个例子
reg = /(\d{4})-(\d{2})-(\d{2})/
// 这样我们就把正则写好了。考虑到之前的例子,我们需要将第三个分组放在开头,第二个分组位置不变,第一个分组放在最后
// 如何做
'2018-12-23'.replace(reg, '$3/$2/$1') // "23/12/2018"
由上述例子可以得知,反向引用就是用 '$1' 获取第一个分组 '$2' 获取第二个分组...以此类推
注意;是 '$1' 代表一个分组,而不是 $1,这里需要注意一下
有时候,我们根本不需要捕获一个分组,就如刚才 reg = /mis(s|t)yyyy/ 一种情况。我们只是想用分组实现一下 或 操作。 没有分组的必要,其次,当正则表达式变得复杂起来,保持明显的分组是很有必要的。
这个时候,我们可以在分组的括号里面加上 ?: 这样就可以取消捕获了
reg = /mis(s|t)yyyy/
'mistyyyy'.replace(reg, '$1') // 't'
// 取消捕获
reg = /mis(?:s|t)yyyy/
'mistyyyy'.replace(reg, '$1') // '$1'
我们可以看下图片的比较,没有分组了。说明取消了捕获。


正向匹配和反向匹配
先解释这两个概念,我们都知道在 JavaSCript 中,正则表达式匹配的顺序是顺着目标字符串进行匹配。
如果我们需要设计一些带条件的匹配规则,比如说:我们需要匹配字符串 'mistyyy' 后面必须是 'good'
举个例子:
reg = /mistyyyygood/
这个时候,'mistyyyy' 后面是 'good' 但是此时,'good' 也被匹配到了,如果我们用 replace 做替换,那么 good 也会被替换掉。
要满足这样的条件。我们可以使用正向匹配
规则如下:
- /expression(?=condition)/ 这表示expression后面必须紧跟 condition 作为条件。
- /expression(?!condition)/ 这表示expression后面必须不是 condition 作为条件。
举个例子;
str = 'mistyyyygood boy'
/mistyyyy(?=good)/.test(str) // true
str.replace(/mistyyyy(?=good)/, 'you') // yougood boy
/mistyyyy(?!good)/.test(str) // false
注意:此时,condition 只是作为条件进行筛选,并不会被匹配到。
我们可以看一个例子
str = 'mistyyyygood boy'
str.replace(/mistyyyy(?=good)/, '$1') // $1good boy
// 我们可以看出条件是不会被匹配到到。
反向匹配,与正向匹配的规则相反,该特性是 ES 2018 新加的特性。
规则如下:
- /(?<=condition)expression/ 表示 expression 前必须满足 condition 条件才匹配
- /(?<!condition)expression/ 表示 expression 前必须不满足 condition 条件才匹配
详解 regExp 对象属性
我们随便写一个正则,看一下打印出来的正则表达式的属性有哪些
reg = /\u0002/yimgus
{
dotAll: true,
flags: 'gimsuy',
global: true,
ignoreCase: true,
lastIndex: 0,
multiline: true,
source: '\u0002',
sticky: true,
unicode: true,
__proto__: Object
}
dotAll,global,ignoreCase,multiline,stricky,unicode 分别代表修饰词 s,g,i,m,y,u 是否出现在正在表达式的修饰词位置。
flags 表示出现的修饰词。
source 表示正则表达式的规则主体部分。
lastIndex 个人认为最重要的属性就是该属性。下面会围绕该属性展开拓展一下。
我们先看个奇怪的例子:
reg = /\d{2}/g
str = '12sd'
reg.lastIndex // 0
reg.test(str) // true
reg.lastindex // 2
reg.test(str) // false
reg.lastindex // 0
lastIndex的值类型是 number 类型。可以进行读写操作。
举个例子:
reg = /\d{2}/
reg.lastindex // 0
reg.lastIndex = 2
reg.lastindex // 2
该属性的含义是从目标字符串的 lastIndex 位置开始进行匹配。但是这是有限制的,只有当修饰符存在 g 或者 y 的时候,才会起作用。
举个例子:
reg = /\d{2}/
str = '12sd'
reg.lastIndex = 2
reg.test(str) // true
reg.lastIndex // 2
reg =/\d.\d/s
str = '1\n2'
reg.lastIndex = 2
reg.test(str) // true
reg.lastIndex // 2
reg = /\d{2}/g
str = '12sd'
reg.lastIndex = 2
reg.test(str) // false
reg.lastIndex // 0
reg = /\d{2}/y
str = '12sd'
reg.lastIndex = 2
reg.test(str) // false
reg.lastIndex // 0
通过以上的例子,我们可以看出,lastIndex 属性只影响了 修饰符 g 和 修饰符 y 的匹配结果。
也就是说:只有这两种的形式是从目标字符串的 lastIndex 位置进行匹配的,其他的修饰符会忽略这个属性。
而且,这两种修饰符匹配失败了,lastIndex 会重置为0。
由此可见,g 修饰符 和 修饰符 y 有相同的作用。那么我们来探寻一下他们的异同点。
相同点:
- 他们都会受 lastIndex 的属性值影响正则开始匹配的效果。即影响从目标字符串的何处开始匹配
- 他们在匹配失败后 lastindex 的属性都会置为 0。
- 他们匹配成功后,lastIndex 都会重置为匹配成功的字符串的下一个字符。
- 修饰符 y 和 g 都不受元字符 ^ 从目标字符串开始的限制,它只受限于 lastIndex 的位置开始匹配。并且只从 lastIndex 的位置开始匹配。
reg1 = /\d/g
reg2 = /\d/y
str = '1ssss'
reg1.lastIndex = reg2.lastIndex = 1
reg1.test(str) // false
reg2.test(str) // false
reg1.lastindex // 0
reg2.lastindex // 0
// 说明都受 lastIndex 的影响,且匹配失败都会置为0
reg1.test(str) // true
reg2.test(str) // true
reg1.lastIndex // 1
reg2.lastIndex // 1
// 匹配成功后,lastIndex 都会重置为匹配成功的字符串(chharAt(0))的下一个字符
reg1 = /^\d/g
reg2 = /^\d/y
str = '1ssss1'
reg1.lastIndex = reg2.lastIndex = 1
reg1.test(str) // false
reg2.test(str) // false
不同点:
修饰符 g 是全局匹配,即匹配到目标字符串不会停止,继续匹配下去,直到没有找到所有符合规则的为止。修饰符 y 不是全局匹配,找到符合规则的就会停止。
举个例子:
reg1 = /\d/g
reg2 = /\d/y
str = '1sssss1'
reg1.test(str) // true
reg1.lastIndex // 1
reg1.test(str) // true
reg1.lastIndex // 7
reg2.test(str) // true
reg2.lastindex // 1
reg2.test(str) // false
reg2.lastindex // 0
// 说明 修饰符 g 是全局匹配,而 y 不是全局匹配。
总结
关于正则表达式,我们讲了一些常见的语法和一些比较生涩的疑难点。对于正则表达式,我们掌握了这些知识点,并不能完全发挥其应有的实力。
我们还应该掌握,应用正则的一些方法有:String 类型的 split,replace,match,search 。RegExp 类型的 test,exec 方法。
真正了解这些方法的应用,才能让正则表达式的强大威力。这些方法,这里暂时不讲了,小伙伴们应该熟悉这些方法的使用,利用他们组合正则表达式,展示出强大的威力。
此外,正则表达式常见的应用场景有模版的解析,dom节点的提取分离,这里面含有大量复杂的正则表达式。如果小伙伴需要精通掌握正则表达式,可以阅读sizzle这个库,该库是 JQ 的核心部分,专门处理复杂的 dom selector,希望我们可以继续努力,将正则表达式真正的掌握。这样,我们就可以写出更加优雅,更加具有可读性的代码了。