JavaScript正则表达式精华总结
写在前面
正则表达式(Regular Expression)是一种文本模式,包括普通字符(例如,a 到 z 之间的字母)和特殊字符(称为”元字符”)。
正则表达式使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串。
正则表达式是繁琐的,但它是强大的,学会之后的应用会让你除了提高效率外,会给你带来绝对的成就感。只要认真阅读本文,加上应用的时候进行一定的参考,掌握正则表达式不是问题。
创建正则表达式的两种方式
1. 字面量方式
1 | let reg = /\d/g |
如上代码所示,即是字面量方式创建正则表达式。
2. 构造函数方式
1 | let reg = new RegExp('\d', 'g') |
RegExp 为创建正则表达式的构造函数,有两个参数,第二个参数不是必传的。第一个参数是一个字符串,指定了正则表达式的模式或其他正则表达式,第二个参数是一个可选的字符串,包含属性 “g”、”i” 和 “m”,分别用于指定全局匹配、区分大小写的匹配和多行匹配。ECMAScript 标准化之前,不支持 m 属性。如果第一个参数是正则表达式,而不是字符串,则必须省略该参数。
返回值为一个新的 RegExp 对象,具有指定的模式和标志。如果第一个参数是正则表达式而不是字符串,那么 RegExp() 构造函数将用与指定的 RegExp 相同的模式和标志创建一个新的 RegExp 对象。
如果不用 new 运算符,而将 RegExp() 作为函数调用,那么它的行为与用 new 运算符调用时一样,只是当第一个参数是正则表达式时,它只返回第一个参数,而不再创建一个新的 RegExp 对象。
元字符
字符 | 描述 |
---|---|
\ | 将下一个字符标记为一个特殊字符、或一个原义字符、或一个 向后引用、或一个八进制转义符。例如,’n’ 匹配字符 “n”。’\n’ 匹配一个换行符。序列 ‘\‘ 匹配 “" 而 “(“ 则匹配 “(“。 |
^ | 匹配输入字符串的开始位置。如果设置了 RegExp 对象的 Multiline 属性,^ 也匹配 ‘\n’ 或 ‘\r’ 之后的位置。 |
$ | 匹配输入字符串的结束位置。如果设置了RegExp 对象的 Multiline 属性,$ 也匹配 ‘\n’ 或 ‘\r’ 之前的位置。 |
* | 匹配前面的子表达式零次或多次。例如,zo* 能匹配 “z” 以及 “zoo”。* 等价于{0,}。 |
+ | 匹配前面的子表达式一次或多次。例如,’zo+’ 能匹配 “zo” 以及 “zoo”,但不能匹配 “z”。+ 等价于 {1,}。 |
? | 匹配前面的子表达式零次或一次。例如,”do(es)?” 可以匹配 “do” 或 “does” 。? 等价于 {0,1}。 |
{n} | n 是一个非负整数。匹配确定的 n 次。例如,’o{2}’ 不能匹配 “Bob” 中的 ‘o’,但是能匹配 “food” 中的两个 o。 |
{n,} | n 是一个非负整数。至少匹配n 次。例如,’o{2,}’ 不能匹配 “Bob” 中的 ‘o’,但能匹配 “foooood” 中的所有 o。’o{1,}’ 等价于 ‘o+’。’o{0,}’ 则等价于 ‘o*’。 |
{n,m} | m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。例如,”o{1,3}” 将匹配 “fooooood” 中的前三个 o。’o{0,1}’ 等价于 ‘o?’。请注意在逗号和两个数之间不能有空格。 |
? | 当该字符紧跟在任何一个其他限制符 (*, +, ?, {n}, {n,}, {n,m}) 后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如,对于字符串 “oooo”,’o+?’ 将匹配单个 “o”,而 ‘o+’ 将匹配所有 ‘o’。 |
. | 匹配除换行符(\n、\r)之外的任何单个字符。要匹配包括 ‘\n’ 在内的任何字符,请使用像”(.|\n)”的模式。 |
(pattern) | 匹配 pattern 并获取这一匹配。所获取的匹配可以从产生的 Matches 集合得到,在VBScript 中使用 SubMatches 集合,在JScript 中则使用 $0…$9 属性。要匹配圆括号字符,请使用 ‘′或′’。 |
(?:pattern) | 匹配 pattern 但不获取匹配结果,也就是说这是一个非获取匹配,不进行存储供以后使用。这在使用 “或” 字符 (|) 来组合一个模式的各个部分是很有用。例如, ‘industr(?:y|ies) 就是一个比 ‘industry|industries’ 更简略的表达式。 |
(?=pattern) | 正向肯定预查(look ahead positive assert),在任何匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如,”Windows(?=95|98|NT|2000)”能匹配”Windows2000”中的”Windows”,但不能匹配”Windows3.1”中的”Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。 |
(?!pattern) | 正向否定预查(negative assert),在任何不匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如”Windows(?!95|98|NT|2000)”能匹配”Windows3.1”中的”Windows”,但不能匹配”Windows2000”中的”Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。 |
(?<=pattern) | 反向(look behind)肯定预查,与正向肯定预查类似,只是方向相反。例如,”(?<=95|98|NT|2000)Windows”能匹配”2000Windows”中的”Windows”,但不能匹配”3.1Windows”中的”Windows”。 |
(?<!pattern) | 反向否定预查,与正向否定预查类似,只是方向相反。例如”(?<!95|98|NT|2000)Windows”能匹配”3.1Windows”中的”Windows”,但不能匹配”2000Windows”中的”Windows”。 |
x|y | 匹配 x 或 y。例如,’z|food’ 能匹配 “z” 或 “food”。’(z|f)ood’ 则匹配 “zood” 或 “food”。 |
[xyz] | 字符集合。匹配所包含的任意一个字符。例如, ‘[abc]’ 可以匹配 “plain” 中的 ‘a’。 |
[^xyz] | 负值字符集合。匹配未包含的任意字符。例如, ‘[^abc]’ 可以匹配 “plain” 中的’p’、’l’、’i’、’n’。 |
[a-z] | 字符范围。匹配指定范围内的任意字符。例如,’[a-z]’ 可以匹配 ‘a’ 到 ‘z’ 范围内的任意小写字母字符。 |
[^a-z] | 负值字符范围。匹配任何不在指定范围内的任意字符。例如,’[^a-z]’ 可以匹配任何不在 ‘a’ 到 ‘z’ 范围内的任意字符。 |
\b | 匹配一个单词边界,也就是指单词和空格间的位置。例如, ‘er\b’ 可以匹配”never” 中的 ‘er’,但不能匹配 “verb” 中的 ‘er’。 |
\B | 匹配非单词边界。’er\B’ 能匹配 “verb” 中的 ‘er’,但不能匹配 “never” 中的 ‘er’。 |
\cx | 匹配由 x 指明的控制字符。例如, \cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 ‘c’ 字符。 |
\d | 匹配一个数字字符。等价于 [0-9]。 |
\D | 匹配一个非数字字符。等价于 [^0-9]。 |
\f | 匹配一个换页符。等价于 \x0c 和 \cL。 |
\n | 匹配一个换行符。等价于 \x0a 和 \cJ。 |
\r | 匹配一个回车符。等价于 \x0d 和 \cM。 |
\s | 匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。 |
\S | 匹配任何非空白字符。等价于 [^ \f\n\r\t\v]。 |
\t | 匹配一个制表符。等价于 \x09 和 \cI。 |
\v | 匹配一个垂直制表符。等价于 \x0b 和 \cK。 |
\w | 匹配字母、数字、下划线。等价于’[A-Za-z0-9_]’。 |
\W | 匹配非字母、数字、下划线。等价于 ‘[^A-Za-z0-9_]’。 |
\xn | 匹配 n,其中 n 为十六进制转义值。十六进制转义值必须为确定的两个数字长。例如,’\x41’ 匹配 “A”。’\x041’ 则等价于 ‘\x04’ & “1”。正则表达式中可以使用 ASCII 编码。 |
\num | 匹配 num,其中 num 是一个正整数。对所获取的匹配的引用。例如,’(.)\1’ 匹配两个连续的相同字符。 |
\n | 标识一个八进制转义值或一个向后引用。如果 \n 之前至少 n 个获取的子表达式,则 n 为向后引用。否则,如果 n 为八进制数字 (0-7),则 n 为一个八进制转义值。 |
\nm | 标识一个八进制转义值或一个向后引用。如果 \nm 之前至少有 nm 个获得子表达式,则 nm 为向后引用。如果 \nm 之前至少有 n 个获取,则 n 为一个后跟文字 m 的向后引用。如果前面的条件都不满足,若 n 和 m 均为八进制数字 (0-7),则 \nm 将匹配八进制转义值 nm。 |
\nml | 如果 n 为八进制数字 (0-3),且 m 和 l 均为八进制数字 (0-7),则匹配八进制转义值 nml。 |
\un | 匹配 n,其中 n 是一个用四个十六进制数字表示的 Unicode 字符。例如, \u00A9 匹配版权符号 (?)。 |
正则的捕获exec
exec() 方法用于检索字符串中的正则表达式的匹配。
1 | RegExpObject.exec(string) |
参数 | 描述 |
---|---|
string | 必需。要检索的字符串。 |
返回值:返回一个数组,其中存放匹配的结果。如果未找到匹配,则返回值为 null。
(1)懒惰性
来看一个例子:
1 | let reg = /\d+/ |
我们可以发现,每次匹配到的结果都是相同的,而且索引index的值一直没变,始终指向第一次匹配到的字符串的起始位置,这就是正则的懒惰性,只匹配第一次匹配到的结果,不再向后匹配。
怎么解决懒惰性问题呢?很容易,加个全局修饰符即可。
1 | let reg = /\d+/g |
我们可以看到,112,456,789这三个数字在原始字符串中都被捕获到了,并且每次的index值也发生了变化;第四个输出为null,表示捕获结束,后面再没有可以捕获的内容了。如此就解决了正则的懒惰性问题。
(2)贪婪性
还是以上述例子来讲:
1 | let reg = /\d+/g |
如果,我们现在想单独捕获到每一个数字,即1,1,2,4,5,6,7,8,9,此时该怎么办呢?
若像上述例子中那样捕获到,则捕获到的内容是数字112,456,789,这三个三位数,显然这把满足要求的连续数字都给一次性捕获到了,这其实就是正则的贪婪性。
那么,如何解决正则的贪婪性呢?即,我们要单独获取到1,1,2,4,5,6,7,8,9这些单个数字:
1 | let reg = /\d+?/g |
解决贪婪性也很简单,即在+后面加一个问号(?)即可。
解决正则的贪婪性很简单,即在量词元字符的后面加问号(?)即可,此例中的量词元字符为+。
正则捕获(字符串的match方法)
match() 方法可在字符串内检索指定的值,或找到一个或多个正则表达式的匹配。
该方法类似 indexOf() 和 lastIndexOf(),但是它返回指定的值,而不是字符串的位置。
1 | stringObject.match(searchvalue) |
参数 | 描述 |
---|---|
searchvalue | 必需。规定要检索的字符串值。 |
regexp | 必需。规定要匹配的模式的 RegExp 对象。如果该参数不是 RegExp 对象,则需要首先把它传递给 RegExp 构造函数,将其转换为 RegExp 对象。 |
返回值存放匹配结果的数组。该数组的内容依赖于 regexp 是否具有全局标志 g。
match() 方法将检索字符串 stringObject,以找到一个或多个与 regexp 匹配的文本。这个方法的行为在很大程度上有赖于 regexp 是否具有标志 g。
如果 regexp 没有标志 g,那么 match() 方法就只能在 stringObject 中执行一次匹配。如果没有找到任何匹配的文本, match() 将返回 null。否则,它将返回一个数组,其中存放了与它找到的匹配文本有关的信息。该数组的第 0 个元素存放的是匹配文本,而其余的元素存放的是与正则表达式的子表达式匹配的文本。除了这些常规的数组元素之外,返回的数组还含有两个对象属性。index 属性声明的是匹配文本的起始字符在 stringObject 中的位置,input 属性声明的是对 stringObject 的引用。
如果 regexp 具有标志 g,则 match() 方法将执行全局检索,找到 stringObject 中的所有匹配子字符串。若没有找到任何匹配的子串,则返回 null。如果找到了一个或多个匹配子串,则返回一个数组。不过全局匹配返回的数组的内容与前者大不相同,它的数组元素中存放的是 stringObject 中所有的匹配子串,而且也没有 index 属性或 input 属性。
注意:在全局检索模式下,match() 即不提供与子表达式匹配的文本的信息,也不声明每个匹配子串的位置。如果您需要这些全局检索的信息,可以使用 RegExp.exec()。
还是以上述的例子来说:
1 | let reg = /\d+/g |
可以看到,一次性把满足条件的内容全部都给捕获到了,然后放在了一个数组中。
再来看,消除贪婪性后的match方法捕获的结果:
1 | let reg = /\d+?/g |
可以看到,满足条件的内容都捕获到了。
match的缺点
match方法虽然很好用,但是在分组捕获中满足子表达式的内容,使用match方法是无法捕获到的。继续往下看分组捕获时会仔细讲解match。
分组捕获
我们已经知道直接在字符后面加上限定符就可以重复单个字符,那么多个字符的重复又该如何实现呢?你可以使用小括号来指定子表达式(也称为分组),然后对于这个子表达式的重复次数你就可以自行规定了,子表达式也可以进行一些其他的操作,这个在后面会进行介绍。
(1)改变优先级
来看一个例子:
1 | let reg = /^15|16$/ |
很显然,以15开头或者以16结尾的所有字符串都符合条件。那如果我们的原意是想匹配156或116呢?此时只需要加个小括号进行分组即可,修改如下:
1 | let reg = /^1(5|1)6$/ |
很显然此时只匹配156或116了。加了小括号之后改变了|元字符的作用范围。
(2)后向引用
假设现在我们想匹配一个字符串中连续重复的字符该怎么办呢?此时,我们便可以利用正则表达式分组中的后向引用。
对一个正则表达式模式或部分模式两边添加圆括号将导致相关匹配存储到一个临时缓冲区中,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 \n 访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数。
那么,对于想要匹配一个字符串中连续重复的字符,我们可以利用后向引用来解决:
1 | let reg = /(\w)\1+/g |
其中 reg 中的 \1 是对前面第一个小括号里面匹配到的内容的引用,于是就解决重复的问题。
例如, /(a)(b)\1\2/可以匹配”abab”,其中 \1,\2分别是对第一个小括号(第一个分组)和第二个小括号(第二个分组)里面匹配到的内容的引用。
需要注意的是,如果引用了越界或者不存在的编号的话,就被被解析为普通的表达式:
1 | var reg = /(\w{3}) is \6/; |
(3)非捕获型分组
使用非捕获元字符 ?:、?= 或 ?! 来重写捕获,忽略对相关匹配的保存。
在一个分组中最开头使用?开头,则此分组为非捕获型分组,即此分组匹配结果不会被缓存,使用exec也不会捕获到此分组内的内容,此分组不存在分组序号,即此分组不存在 \n ,也就是说不存在后向引用的操作。
正向前瞻型分组:你站在原地往前看,如果前方是指定的东西就返回true,否则为false。
1 | var reg = /kid is a (?=doubi)/ |
反向前瞻型分组:你站在原地往前看,如果前方不是指定的东西则返回true,如果是则返回false。
1 | var reg = /kid is a (?!doubi)/ |
假如现在有这样一个需求,将一个数字字符串每三位中间以逗号分隔,例如,123456789变为123,456,789这样。
1 | let reg = /\B(?=(\d{3})+(?!\d))/g |
字符串replace方法
replace() 方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。
1 | stringObject.replace(regexp/substr,replacement) |
参数 | 描述 |
---|---|
regexp/substr | 必需。规定子字符串或要替换的模式的 RegExp 对象。 |
请注意,如果该值是一个字符串,则将它作为要检索的直接量文本模式,而不是首先被转换为 RegExp 对象。
replacement 必需。一个字符串值。规定了替换文本或生成替换文本的函数。
返回值为一个新的字符串,是用 replacement 替换了 regexp 的第一次匹配或所有匹配之后得到的。
当第一个参数为字符串时,每次替换只会替换第一次匹配到的字符串,例如:
1 | let str = 'ggghhhggg' |
那若是我们想要替换掉所有匹配到的内容呢?此时第一个参数则需要传入一个正则表达式,例如:
1 | let str = 'ggghhhggg' |
第二个参数除了传字符串之外,还可以传入一个函数,我们来看:
1 | let str = 'ggg123hhh456ggg78' |
可见输出了两次,分为为匹配到的123和456。
我们使用exec和match分别来匹配试一下:
1 | let str = 'ggg123hhh456ggg78' |
综上所述,我们不难看出 replace 函数的第二个参数为函数时,此匿名函数的参数为 replace 函数的第一个参数的正则表达式对原始字符串每次执行 exec 后的结果。
此时,也可以点出match函数虽然可以直接得到符合条件的数组,这一点非常好用,毋庸置疑。但是 match 函数的的不足之处在于,它无法捕获到分组中的小正则表达式匹配的内容,如此例中的(\d)匹配到的内容,match 方法无法捕获到,但是小正则中匹配到的内容,exec 函数却可以捕获到。
假如此例中,我们需要把连续三个数字替换为666,则我们可以利用 replace 函数这样做:
1 | let str = 'ggg123hhh456ggg78' |
replace 函数每次返回的值即为替代每次匹配到的内容。