JavaScript 正则表达式总结

很长一段时间没有总结写博客了,也不是因为有多忙,而是在闲暇的时间人就变懒了,各种电影、游戏打发了时间。经过一段时间的低迷,又重新燃气了学习的欲望,所以写博客这个习惯还是要保持下去。前不久为了学习 PVP 游戏开发买了一个阿里云的服务器,闲下来后觉得有一些浪费,准备干点儿什么。因为平时混迹各种平台,账号太多记不住,打算写一个简单的网页,用于管理这些账号,于是又开始了 Web 的学习。在学到 JavaScript 正则表达式的时候发现自己的基础还是不太好,以前的东西基本上忘了,所以把正则表达式好好“复习”了一边。

什么是正则表达式

正则表达式是由一个字符序列形成的搜索模式,当你在文本中搜索数据时,你可以用搜索模式来描述你要查询的内容。正则表达式可以是一个简单的字符,或一个更复杂的模式。正则表达式可用于所有文本搜索和文本替换的操作。

大部分编程语言都支持正则表达式,每种语言又有细节上的差异,本文只对 Java 和 JavaScript 的正则表达式做一个总结。

语法

在 JavaScript 中,正则表达式的形式是:

/表达式/[修饰符]

其中表达式是主体,修饰符可以省略。

简单模式

先来一个最简单的正则表达式:

1
2
3
var regex = /boy/;
var str = "I am a beautiful boy.";
console.log(str.match(regex));

match() 函数返回的是 RegExpMatchArray 对象,里面记录了这一次匹配的结果,关于 match() 函数还有其他和正则表达式有关的函数后面再详细介绍。

执行上面的代码(本人使用 node.js 命令行执行),控制台会出现如下信息:

1
[ 'boy', index: 17, input: 'I am a beautiful boy.' ]

结果的含义很明显,’boy’ 表示满足表达式匹配到的字符串,index 表示匹配到第一个字符的位置,从 0 开始,input 是输入的要匹配的字符串。

这是正则表达式最简单的模式,直接匹配指定的字符串,也是用的较多的一种模式。

修饰符

修饰符用于增加正则表达式的匹配能力,有三个修饰符可以选择:

  • i:忽略大小写
  • g:全局匹配
  • m:多行匹配

修饰符 i

忽略大小写,这个很好理解,就拿上一节的例子来说,稍微改动一下:

1
2
3
var regex = /boy/;
var str = "I am a beautiful Boy.";
console.log(regex.test(str));

这里有一个新的函数 test(),用于测试一个字符串是否能匹配某个正则表达式,返回 true 或 false。

执行代码,返回 false,如果加上 i 修饰符:

1
2
3
var regex = /boy/i;
var str = "I am a beautiful Boy.";
console.log(str.match(regex));

此时结果返回 true,这样匹配过程忽略了大小写。

修饰符 g

正则表达式在默认情况下搜索到第一个匹配的字符串就会停止,加上 g 修饰符便会进行全局匹配,直到字符串被搜索完,来一个例子:

1
2
3
var regex = /a/g;
var str = "I am a beautiful boy.";
console.log(str.match(regex));

上面的代码如果不加修饰符 g,结果只有一个 ‘a’,加上 g 之后会匹配到三个 ‘a’,这便是全局匹配。

修饰符 m

使用这个修饰符之前要先了解下另外两个元字符:^ 和 $,这两个元字符分别表示要匹配的字符串必须以 ^ 后面的表达式开始,以 $ 前面的表达式结束,拿 ^ 举例来说:

1
2
3
var regex = /^a/;
var str = "I am a beautiful boy.";
console.log(regex.test(str));

上面的代码如果不加元字符 ^,结果返回 true。但加上之后表示要匹配的字符串必须以 ‘a’ 为第一个字符,所以返回 false。$ 的用法类似。

元字符是正则表达式的特殊字符,用于指定各种匹配规则,关于这部分后面再详细介绍。

有什么办法让其匹配成功呢?m 修饰符可以上场了,m 修饰符的作用是修改 ^ 和 $ 在正则表达式中的作用,让它们分别表示行首和行尾。意思就是加上 m 修饰符,如果目标字符串中有换行符 \n,则 ^ 和 $ 可以对每一行进行匹配,修改下上面的例子:

1
2
3
var regex = /^a/m;
var str = "I \nam a beautiful boy.";
console.log(regex.test(str));

执行代码会返回 true,’\na’ 表示第二行,加上 m 修饰符后便是多行匹配,只要行首是 ‘a’,则匹配成功。

混合使用

修饰符可以混合使用,再修改下上面的例子:

1
2
3
var regex = /^a/gm;
var str = "I \nam \na beautiful boy.";
console.log(str.match(regex));

执行上面的代码,控制台输出结果:

1
[ 'a', 'a' ]

很好理解,不做解释。

RexExp 对象

RexExp 是 JavaScript 中用来控制正则表达式的对象,其构造方式有以下两种:

1
var regex = /表达式/[修饰符];
1
var regex = new RegExp("表达式"[, "修饰符"]); // 注意表达式和修饰符这时是一个字符串,要加单引号或双引号

这两种构造方式构造出来的正则表达式没有任何区别,但在不需要改变对象的情况下建议使用第一种,性能会略微好一点。

属性

RegExp 对象有几个内置属性:

  • global:该对象是否开启全局匹配模式,布尔值
  • ignoreCase:该对象是否开启忽略大小写模式,布尔值
  • multiline:该对象是否开启多行模式,布尔值
  • lastIndex:该对象下一次开始匹配的位置,即上一次匹配结束的位置(匹配到的字符串的末尾位置),正整数
  • source:该对象的源文本,不包含修饰符,字符串

函数

RegExp 对象有几个函数:

test()

上面使用过这个函数,用于测试一个字符串是否能匹配某个正则表达式,参数为字符串,返回 true 或 false。该函数将忽略 g 修饰符,只要搜索到符合正则表达式的字符串即匹配成功。如果正则表达式含有 g 修饰符,那么每次使用 test() 将从 lastIndex 位置开始匹配,匹配成功会更新 lastIndex 属性值。若没有 g 修饰符,test() 将从字符串起点开始匹配,也不会更新 lastIndex 属性值。

1
2
3
4
var regex = /am/;
var str = "I am a beautiful boy.";
console.log(regex.test(str) + ", " + regex.lastIndex);
console.log(regex.test(str) + ", " + regex.lastIndex);

执行上面的代码,控制台输出如下信息:

1
2
true, 0
true, 0

给正则表达式加上 g 修饰符:

1
2
3
4
var regex = /am/g;
var str = "I am a beautiful boy.";
console.log(regex.test(str) + ", " + regex.lastIndex);
console.log(regex.test(str) + ", " + regex.lastIndex);

再次执行代码,控制台输出如下:

1
2
true, 4
false, 0

exec()

搜索一个字符串中的符合正则表达式的匹配,参数为字符串,返回 RegExpExecArray 对象,记录了匹配的结果。该函数同样忽略 g 修饰符,若正则表达式中没有 g 修饰符,exec() 将从字符串的起始位置搜索,只要搜索到符合正则表达式的字符串就停止,也不更新 lastIndex 属性。如果有 g 修饰符,会从 lastIndex 位置开始搜索,搜到到符合正则表达式的字符串便停止,而且更新 lastIndex 属性。这个原理和 test() 函数其实是一样的,不再举例。

RegExpExecArray 对象中有一个 index 属性,这个属性是记录匹配到的字符串的开始位置在原始字符串中的索引,而 RegExp 对象中的 lastIndex 属性是记录匹配到的字符串的末尾位置在原始字符串中的索引,这个上面有提及到。

字符串对象

正则表达式服务的就是字符串,所以要使用好正则表达式,需要对字符串有一定的了解。本文只对字符串中几个和正则表达式有关的函数进行介绍。

搜索一个字符串中的符合正则表达式的匹配,参数可以是正则表达式,也可以是字符串,返回匹配到的字符串的开始位置在原始字符串中的索引。该函数会忽略正则表达式中的 g 修饰符和 RegExp 对象中的 lastIndex 属性,即总是会从字符串的起始位置搜索,也不更新 lastIndex 属性,如果未搜索到返回 -1。

1
2
3
var regex = /am/;
var str = "I am a beautiful boy.";
console.log(str.search(regex));

执行上面代码,返回结果为 2。

match()

搜索一个字符串中的符合正则表达式的一个或多个匹配,参数可以是正则表达式,也可以是字符串,返回 RegExpMatchArray 对象,记录了匹配的结果。该函数忽略 RegExp 对象中的 lastIndex 属性,总是会从字符串的起始位置搜索,不更新 lastIndex 属性。如果正则表达式中没有 g 修饰符,只搜索到第一个匹配的字符串就会停止。如果有 g 修饰符,那么会匹配结果会是一个数组,包含所有能匹配到的字符串。

1
2
3
var regex = /a/g;
var str = "I am a beautiful boy.";
console.log(str.match(regex));

执行上面的代码,控制台输出如下:

1
[ 'a', 'a', 'a' ]

RegExpMatchArray 对象中也有一个 index 属性,记录匹配到的字符串的开始位置在原始字符串中的索引。

字符串对象的 match() 函数和 RegExp 对象的 exec() 函数功能很相似,区别在于 match() 函数不需要关心上一次匹配后记录的位置索引,而且在某些情况下搜索到一个匹配结果后还能继续搜索匹配。

split()

字符串分割,按照正则表达式将一个字符串分割成若干个,参数有两个,第一个可以是正则表达式,也可以是字符串,第二个用于指定返回数组的个数,默认为最大个数,函数返回一个数组。该函数只会分割一次,没有全局分割和上一次分割的概念。

1
2
3
var regex = /a/g;
var str = "I am a beautiful boy.";
console.log(str.split(regex, 3));

执行上面代码,控制输出如下信息:

1
[ 'I ', 'm ', ' be' ]

字符串对象还有一个 replace() 函数,该函数用法较为复杂,和很多元字符相关联,后面再详细介绍。

字符串的函数有一个共同点,就是参数可以是正则表达式,也可以是字符串,如果传入字符串,其实就是简单模式的正则表达式(没有任何元字符和修饰符)。

元字符

正则表达式中的元字符是包含特殊含义的字符,它们有一些特殊的功能,可以控制字符串匹配的方式。如果想要使元字符用作普通字符,在其前面加上反斜杠。

转义字符

转义字符只有一个:\,主要用于将元字符标记为普通字符,比如 /\d/(下节将要介绍)是匹配所有数字,如果改为 /\d/,则只会匹配 ‘\d’ 这个字符串。

单个字符

这类元字符只能匹配字符串中的一个字符:

元字符 匹配规则
. 匹配除换行符以外的所有字符
匹配该元字符两端的任意一个字符,比如 x或yood 可以匹配 ‘xood’ 或者 ‘yood’
[] 匹配中括号里的任意字符,里面的内容可以是字符,也可以是一个范围,用破折号 - 表示区间,比如 [0-9a-z] 表示匹配所有的数字和小写字母的字符。中括号内的某些元字符是不起作用的,当作普通字符匹配
[^] 功能同上,含义相反,比如 [^0-9a-z] 表示匹配所有非数字、非小写字母的字符
\d 匹配所有数字,等价于 [0-9]
\D 匹配所有非数字,等价于 [^0-9]
\w 匹配所有数字、字母和下划线 _
\W 匹配所有非数字、非字母、非下划线 _

或代表 | 元字符,在 markdown 的表格中出现的 | 会被翻译。

根据上面的元字符已经可以做很多事了,现在来写一个简单的功能,提取一个字符串中所有的手机号码:

1
2
3
var regex = /1[3458]\d\d\d\d\d\d\d\d\d/g;
var str = "number1: 15921453264, number2: 14735675687, number3 : 17898763526, number4: 1351234567, nunmber5: 25921453264";
console.log(str.match(regex));

执行代码,控制台输出如下信息:

1
[ '15921453264', '14735675687' ]

这段代码较简单,第三组数字的第二位不是 3、4、5、8 中的一个,第四组数字只有 10 位,第五组数字的第一位不是 1,所以只有前两组数字被提取出来。

空白字符

这类元字符可以匹配字符串中的各种空白字符:

元字符 匹配规则
\0 匹配 null 字符
\f 匹配进制字符
\n 匹配换行符
\r 匹配回车字符
\t 匹配水平制表符
\v 匹配垂直制表符
\s 匹配空白字符、空格、制表符和换行符
\S 匹配非空白字符

这类元字符较简单,一般结合其他元字符进行使用。

锚字符

这类元字符用来控制要匹配字符串的位置:

元字符 匹配规则
^ 该元字符后面的表达式必须位于目标字符串的行首位置,这个元字符前面一般不能有表达式
$ 该元字符前面的表达式必须位于目标字符串的行尾位置,这个元字符后面一般不能有表达式
\b 匹配单词边界,在该元字符前面或后面的表达式必须是是在单词边界,也就是说表达式前面或后面必须有空格,这个元字符放在 [] 中失效,表示匹配一个退格符
\B 匹配非单词边界,功能同上,含义相反

这类元字符还是较常用的,软件产品中经常会有这样的需求,输入框只能输入手机号码,那么到现在就可以写出这么一个功能:

1
2
3
var regex = /^1[34578]\d\d\d\d\d\d\d\d\d$/;
var str = "15921453264";
console.log(regex.test(str));

执行代码,返回 true,这个正则表达式就是把开始位置和结束位置都限定死了,可以用来检测一个字符串是否为手机号码。

重复字符

这类元字符用来控制要匹配字符串的重复次数:

元字符 匹配规则
{n} n 是一个非负整数,其前面的表达式必须连续出现 n 次
{n,} n 是一个非负整数,其前面的表达式必须至少出现 n 次
{n,m} n 和 m 都是一个非负整数,其前面的表达式必须至少出现 n 次,至多出现 m 次,注意 n 和 m 之间不能有空格
* 匹配前面的表达式零次或多次,等价于 {0,}
+ 匹配前面的表达式一次或多次,等价于 {1,}
? 匹配前面的表达式零次或一次,等价于 {0,1}

有了这类元字符,之前那个检测手机号码的功能可以简化一下代码:

1
2
3
var regex = /^1[34578]\d{9}$/;
var str = "15921453264";
console.log(str.match(regex));

执行代码,返回 true,这样,一些需要匹配的重复出现的规则相同的字符,写起来就简单多了。

分组字符

接下来到了个人认为正则表达式中最难理解的元字符了,这类元字符用来进行分组匹配。

(x)

这种分组叫做捕获性分组,匹配到的所有分组内容都返回。先来看一个列子:

1
2
3
var regex = /ab(cd)ef/;
var str = "abcdefgh";
console.log(str.match(regex));

执行上面代码,控制输出如下信息:

1
[ 'abcdef', 'cd', index: 0, input: 'abcdefgh' ]

通过上面的结果可以发现,加上括号后返回结果多了 ‘cd’,这便是这个元字符的作用,它的过程是先按照全部的正则表达式匹配,如果搜索到匹配结果再将括号内的内容从头匹配。如果没有搜到全部正则表达式的匹配,则不会匹配括号中的内容,比如上面的例子稍微改一下:

1
2
3
var regex = /ab(cd)ef/;
var str = "acdefgh";
console.log(str.match(regex));

执行代码,返回结果为 null。

使用这个元字符还可以使用替代元字符,分为两种:\ 和 $。这两个元字符后面跟一个正整数,代表替代分组中的第几组匹配结果,例如:

1
2
3
var regex = /ab(cd)ef\1/;
var str = "abcdefcdgh";
console.log(str.match(regex));

执行代码,控制台输出如下信息:

1
[ 'abcdefcd', 'cd', index: 0, input: 'abcdefcdgh' ]

因为匹配的分组结果第一组为 ‘cd’,所以全部的正则表达式匹配的时候匹配内容就是 ‘abcdefcd’。

之前在介绍字符串对象函数的时候有一个 replace() 函数没有介绍,现在是时候登场了。这个函数是用来将一个字符串替换成指定字符串的,参数有两个,第一个参数可以是正则表达式,也可以是字符串,第二个参数是将要替换字符串的内容,先来一个简单的用法:

1
2
3
var regex = /abcdef/;
var str = "abcdefgh";
console.log(str.replace(regex, "123"));

执行代码,返回结果是 ‘123gh’,可以看出 ‘abcdef’ 被替换成了 ‘123’,这个很好理解。现在加入捕获性分组元字符,代码如下:

1
2
3
var regex = /ab(cd)ef/;
var str = "abcdefgh";
console.log(str.replace(regex, "123$1"));

执行代码,返回结果是 ‘123cdgh’,将要替换的字符串内容多了 ‘$1’,它代表了分组匹配中的第一组内容。

(?:x)

这种分组叫非捕获性分组,和捕获性分组的区别就是返回结果不包含分组匹配的内容。那这样就有疑问了,这样和不加这个元字符有什么区别?先来看一个例子:

1
2
3
var regex = /zoo+/;
var str = "zoozoo";
console.log(str.match(regex));

执行上面的代码,控制台输出如下:

1
[ 'zoo', index: 0, input: 'zoozoo' ]

上面的正则表达式只能匹配 ‘zo’ 后面跟一个或多个 ‘o’,但很多实际情况想要把整个 ‘zoo’ 当成一个表达式来匹配,这时候如果使用捕获性分组又不想返回分组匹配的内容,使用非捕获性分组是最优选择,改动代码如下:

1
2
3
var regex = /(?:zoo)+/;
var str = "zoozoo";
console.log(str.match(regex));

执行代码,控制台输出如下:

1
[ 'zoozoo', index: 0, input: 'zoozoo' ]

这样的结果就是我们预期所需要的。

其他分组字符

除了上面详细的介绍的两个分组字符,还有一些分组字符也较为常用:

元字符 匹配规则
(?=x) 正向肯定预查,是一个非捕获性分组,该元字符前面的表达式后面必须跟能够正确匹配 x 的表达式,比如 Window(?=2000) 可以正确匹配 ‘Window2000’,但无法匹配 ‘Window 98’
(?!x) 正向否定预查,是一个非捕获性分组,和上面的元字符含义正好相反,该元字符前面的表达式后面一定不能跟能够正确匹配 x 的表达式
(?<=x) 反向肯定预查,是一个非捕获性分组,和正向肯定预查的区别是作用于该元字符前面的表达式,JavaScript 不支持这个元字符
(?<!x) 反向否定预查,是一个非捕获性分组,和正向否定预查的区别是作用于该元字符前面的表达式,JavaScript 不支持这个元字符

惰性字符

正则表达式中匹配模式默认为贪婪的,即尽可能匹配多的字符,比如下面的代码:

1
2
3
var regex = /fo*/;
var str = "fooooo";
console.log(str.match(regex));

执行上面代码,匹配结果为 ‘fooooo’,这个表示贪婪匹配,如果想要改为惰性匹配,在其后面加上元字符 ? 即可:

1
2
3
var regex = /fo*?/;
var str = "fooooo";
console.log(str.match(regex));

执行代码,匹配结果为 ‘f’,尽可能匹配少的字符,这就是惰性字符。

惰性字符只能跟在 *、+、?、{} 这几个元字符后面,如果跟在其他元字符后面表示匹配前面的表达式零次或一次,这个前面有介绍过。

总结

至此,关于 JavaScript 中有关正则表达式的用法就全部介绍完了,正则表达式在程序开发中是比不可少的,了解并深入学习也是很有必要的。

坚持原创技术分享,您的支持将鼓励我继续创作!