我们都爱正则表达式,但是一个个奇怪字符的组合总是让我这种弱鸡感到难以领悟。

每次看到一个正则匹配式却理解不了,我都安慰自己:“反正我已经知道它是用来匹配文本的”

为什么不现在直接把它学会呢?


首先推一个可以帮助你理解正则的网站正则表达式可视化

还有一个 Atom 插件 regex-railroad-diagram

这样的可视化模型可以帮助你快速回忆与熟悉正则字符的含义。

1.字符

正则表达式中我们可以使用两种字符:

  • 原义字符:原义文本字符代表字符本身的匹配
  • 元字符:元字符是正则表达式中有特殊含义的字符,其组合代表特殊字符或者其他逻辑

在正则表达式中以下字符有特殊含义,为元字符
所以只有转义之后它们才能作为原义文本字符来使用:

*, +, ?, $, ^, ., |, \, (), [], {}

比如你想要匹配字符串中出现的$, 必须在正则表达式中使用\$进行转义

特殊字符

这几个组合而成的元字符组合会匹配相应特殊字符:

字符 匹配特殊字符
\t 制表符
\v 垂直制表符
\n 换行符
\r 回车
\f 换页
c* Ctrl+特定字符

2.字符匹配方式

正常匹配:

在正则表达式中使用原义字符或组合成的特殊字符会正常匹配相应字符

在正则表达式中使用a会匹配文本a1b2ab3中第一个字符a和倒数第三个字符a

字段匹配:

在正则表达式中使用连续的原义字符或组合而成的特殊字符会匹配相应字符组合

在正则表达式中使用ab会匹配文本a1b2ab3中倒数第二三个字符ab

类匹配:

在正则表达式中使用元字符:

[ ]: 创建将匹配[ ]中的任意字符

类中字符的连写代表的关系——满足连写的字符其中一者就将会被匹配

正则表达式中使用[ab]会匹配文本a1b2ab3中所有的ab字符

特殊的

  • [^]: 反相匹配——匹配不是[^]中的任意字符
  • [a-z]: 小写字母匹配——从az的小写英文字符
  • [A-Z]: 大写字母匹配——从AZ的大写英文字符
  • [0-9]: 数字匹配——从09的数字字符

同时有一些等价的元字符方便使用:

元字符 等价类
. [^\r\n]
\d [0-9]
\D [^0-9]
\s [\t\n\v\f\r]
\S [^\t\n\v\f\r]
\w [a-zA-Z_0-9]
\W [^a-zA-Z_0-9]

具体什么含义?根据之前的线索推断一下吧 : )

量匹配

有的时候,正则表达式需要对字符出现次数进行匹配,此时我们需要使用量匹配的元字符

字符 含义
? 最多出现一次
+ 至少出现一次
* 出现任意次
{n} 出现n次
{n,m} 出现n到m次
{n,} 至少出现n次

还包含其它的逻辑匹配

字符 结果
x(?=y) 只有当x后面紧跟着y时,才匹配x
x(?!y) 只有当x后面不是紧跟着y时,才匹配x
x|y 匹配xy

量匹配的元字符跟随在字符的后面生效,比如:

T\d?匹配后面最多出现一次数字字符的字符T

进行`{n,m}`的匹配时,会出现重复满足匹配条件的情况, 用正则表达式`a\d{2,4}`对字符串`a123456`进行匹配, 会匹配到`a12`,`a123`,`a1234`中的哪个呢?

默认情况下,正则表达式处于贪婪模式,即尽可能多地匹配字符串,因此匹配结果应该是a1234

如果想进入非贪婪模式,需要在量词后面加一个问号:a\d{2,4}?,此时正则表达式一旦匹配成功就不会继续匹配

此时匹配结果为a12

分组匹配

在正则表达式中使用()可以在正则表达式中生成“组”

“组”在字符组合的时候具有相对的优先级:

当我们的正则表达式复杂的时候,我们需要分组进行元字符和字符的组合:

比如我们使用[a-z]\d{3}匹配a1b2c3d4就是无效的,因为量匹配仅对其前面的字符有效,这个正则表达式的意思就是匹配一个后面跟随三个数字的字母

当我们想要匹配另一种含义——“连续出现三次字母+数字时”,我们就需要采用分组匹配的方式:

使用正则表达式([a-z]\d){3}匹配a1b2c3d4就会得到a1b2c3

同时我们可以使用分组匹配来配合逻辑元字符进行不同逻辑的匹配效果,比如:

使用正则表达式Hel(lo|en)对字符串Hello Helen!进行匹配,匹配得到的结果为

HelloHelen

“组”除了优先级的提升,还有其他的特殊效果:

分组匹配可以在替换的时候配合$进行反向引用,反向引用会在后面的章节进行讲解

同时也可以通过exec方法被分拣到数组里面(见后面章节)

如果我们仅仅想使用括号操作元字符效果的范围,并不想生产分组匹配的“组”并实现相关特殊效果,我们可以使用`(?:`+内容+`)`的括号来达成目的

边界匹配

边界匹配是采用元字符的一种特殊匹配方式:

字符 含义
^ 匹配行首的边界
$ 匹配行末的边界
\b 匹配单词边界
\B 匹配非单词边界

这些字符本身并不匹配字符,匹配的是特定的边界

在字符串There HiThere There中使用(全局模式)

  • 正则表达式^/$结果替换为# —— #There HiThere ThereThere HiThere There#
  • 正则表达式^T/e$结果替换为# —— #here HiThere ThereThere HiThere Ther#
  • 正则表达式\b结果替换为# —— #There# #HiThere# #There#
  • 正则表达式\bT结果替换为# —— #here HiThere #here
  • 正则表达式\bThere\b —— 匹配第一个单词There与最后一个单词There
  • 正则表达式\BThere\b —— 匹配HiThere里面的There

根据这些规律的提示,加上你自己的练习,掌握边界匹配不需要太多时间。

正则表达式默认会将一段文本中每个换行处转化为`\n`换行符,而这不会被`^`和`$`匹配,除非开启了多行模式。有关多行模式的内容,见下一章节。

修饰符

修饰符是针对于每个正则表达式设置的属性(它们之间互相不冲突):

修饰符 含义
g 全局模式:对字符串从头开始不止一次地进行匹配
i 忽略大小写模式:忽略英文字母大小写
m 多行模式:令元字符^$在多行模式下进行工作(行是由\n\r分割的)
y 黏性模式:只从lastIndex位置开始匹配(且不试图从任何之后的索引匹配)
`Javascript`全局模式下重复匹配会有一些奇怪的事情发生(比如下面这段代码)这是因为`lastIndex`属性在全局模式下生效,其具体特性见后面的章节
1
2
3
4
5
6
7
8
9
10
var reg = /\w/g
var reg1 = /\w/
reg.test('a')
//true
reg.test('a')
//false
reg1.test('a')
//true
reg1.test('a')
//true

而且,前文说过,我们之前的实例全部在默认使用全局模式,如果不在全局模式之下,正则表达式只会匹配字符串从头开始的第一个字符段

2.JavaScript 中的 RegExp 对象

JavaScript中,由/开始,由/结束(或者跟随有效正则修饰符)的字段会被JavaScript编译器正确解析为一个RegExp对象。

同时// —— 空的正则表达式会被解析为注释

RegExpJavaScript中的内置对象,其构造函数创建了一个正则表达式对象,用来匹配文本

RegExp的详细特性在mdn上有详细讲解与实例

RegExp有两种构造方式:

  • 字面量方式
  • 构造函数方式

字面量方式

RegExp对象的实例由/+正则表达式正文+/+正则表达式修饰符组成

1
var reg = /\bis\b/g

构造函数

1
var reg = new RegExp('\\bis\\b', 'g')

RegExp构造函数接受两个参数:

  • 包含正则表达式的一个字符串(注意在字符串中转义符需要转义输入)
  • 包含正则表达式修饰符的一个字符串

因此我们更倾向于使用字面量方式进行 RegExp 实例的构建

RegExp对象的属性

JavaScript中,RegExp有以下属性:

  • RegExp.lastIndex
  • RegExp.prototype.global
  • RegExp.prototype.ignoreCase
  • RegExp.prototype.mutiline
  • RegExp.prototype.source
  • RegExp.prototype.sticky

以上属性中,除了lastIndexsource以外,其它属性都描述着RegExp对象的在构造时附件的修饰模式 – 由构造RegExp时附加的修饰符决定(它们都是布尔值)

source属性是RegExp对象正则表达式正文的字符串 —— 正则表达式/内部的内容

lastIndex是个复杂的属性,它相当RegExp上的一个标记,它可写,指定着下一次匹配的起始索引位置,同时也是匹配的上一次结果尾部后一个字符的索引位置(只有在全局模式下该属性才有效)

1
2
3
4
var reg = /\w/
while(reg.test('abc')){
console.log(reg.lastIndex)
}

得到结果为1,2,3

全局正则表达式的匹配可以配合lastIndex进行匹配异步的驱动

有很多与正则表达式有关的方法可以让我们更轻松地进行字符串操作:

RegExp对象的方法

JavaScript中,RegExp有以下方法:

  • RegExp.prototype.exec()
  • RegExp.prototype.test()
  • RegExp.prototype.toString()

exec方法接受一个字符串作为参数,返回一个数组:

  • 第一个元素是正则表达式匹配到的文本
  • 之后的元素都是依次排列的,正则表达式中从前到后每个分组匹配到的文本
  • 数组中存在两个索引:
    • index: 匹配到字段开始的索引
    • input: 匹配的整体字符串
  • 正则表达式匹配不到返回null

全局模式RegExp对象调用exec时,可以多次执行exec方法来查找同一个字符串中的其它的成功匹配,每次exec都会更新lastIndex属性

test方法接受一个字符串作为参数,如果正则表达式可以匹配到字符串里的字段,返回true,否则返回false

exec方法和test方法在全局的RegExp对象执行时都会更新lastIndex:

1
2
3
4
5
6
7
var reg = /\w\d/g,
str = 'a1b2c3d4'
console.log(reg.lastIndex) //0
reg.test(str) //true
console.log(reg.lastIndex) //2
reg.exec(str) // [ 'b2', index: 2, input: 'a1b2c3d4' ]
console.log(reg.lastIndex) // 4

这个表格是测试后的lastIndex变化规律:

lastIndex索引范围 下一次匹配结果 下一次匹配lastIndex的调整
大于字符串的长度 一定失败 0
等于字符串的长度,RegExp不匹配空字符串 一定失败 0
等于字符串的长度,RegExp匹配空字符串 成功匹配到空字符串 不变
小于字符串的长度 在索引位置向后进行匹配的结果 下一次匹配到字段的后一位,匹配不到则归0

toString方法则返回是正则表达式的全文的字符串

其他与RegExp对象相关的方法

  • String.prototype.match()
  • String.prototype.replace()
  • String.prototype.search()
  • String.prototype.split()

matchRegExpexec方法相似,接受一个RegExp对象为参数,返回一个包含匹配结果的数组:

  • 在非全局模式下,返回的数组包含和RegExp.exec一样的内容
  • 在全局模式下,返回包含所有匹配字段的数组
  • 如果不能匹配到,则返回null

replace方法接受两个参数:第一个参数是RegExp或字符串,第二个参数为字符串或函数,字符串调用replace之后会将字符串中第一个参数正则匹配到的/出现的替换为某个字符串,详细请看mdn

第二个参数作为字符串的话可以使用$号来进行反向引用:

1
2
3
'2016-10-10'.replace(/(\d{4})-(\d{2})-(\d{2})/g, '$2$3$1')
// 10102016
'2016-10-10'.replace(/(\d{4})-(\d{2})-(\d{2})/g, '\$2\$3\$1')

除了分组匹配,还有:

变量名 代表的值
$$ 插入个$
$& 插入匹配的子串
`$`` 插入当前匹配的子串左边的内容
$' 插入当前匹配的子串右边的内容

第二个参数作为函数的话有这几个(至少三个以上)个参数:

  • 1个参数:匹配到的文本字符串
  • 2到第n个参数:分组匹配到的内容(设进行了n-1次分组匹配)
  • n+1个参数:匹配项在字符串中的index
  • n+2个参数:原字符串
`replace`的第一个参数,我们如果提供字符串作为参数,就会被自动转化为`RegExp`对象 —— 不附带任何修饰模式,实例如下:
1
2
'a1a2a3a4'.replace('a','b') //b1a2a3a4
'a1a2a3a4'.replace(/a/g,'b') //b1b2b3b4

search方法与RegExptest相似,
接受一个RegExp对象为参数,但是返回值为第一次匹配到字段的索引数值(如果没有匹配到则返回-1

split方法接受两个参数,第一个参数为分隔符(可以是字符串和RegExp),第二个参数为片段数量的限定值(可选),返回一个由一个个分隔符之间的字段组成的数组

如果不存在有效参数,则返回整个字符串在第一位的数组

如果分隔符是一个空字符串,则会把原字符串中每个字符的装在数组里返回

如果分隔符是包含分组匹配的正则表达式,则每次匹配到结果时,分组匹配到的结果也会插入到返回的数组中

1
2
3
4
5
var reg1 = /\*/,
reg2 = /(\*)/,
str = 'c*x*y'
str.split(reg1) // ["c", "x", "y"]
str.split(reg2) // ["c", "*", "x", "*", "y"]

3.总结

写了这么多,笔者相信你们头都晕了 : )

记住正则表达式的关键:

Javascript

正则表达式是用来进行文本匹配相关操作的一个特殊的内建对象

  • 匹配自身的的原义字符
  • 匹配特殊字符或者调整匹配逻辑的元字符

这两种字符的组合成正则表达式的正文,配合修饰符,去匹配字符串中的文本字段