RegExp对象
JavaScript中通过内置对象RegExp支持正则表达式,一般地,我们有两种方法来实例化RegExp对象。
字面量
var expression = /pattern/flags
其中,pattern为正则表达式。flags为修饰符,可以支持一个或多个。
构造函数
在ES5中,RegExp构造函数的参数有两种情况。
var reg1 = new RegExp('abc', 'g') // 第一个参数为字符串,第二个为修饰符
var reg2 = new RegExp(/abc/g) // 参数为一个正则表达式
其中的g
是一个修饰符,表示全局匹配,找到所有满足条件的组合。上述两种书写是等价的。
在ES6中,支持在第一个参数为正则表达式的情况下,第二个参数接受修饰符。此时,会忽略第一个参数中原有的修饰符。
var reg3 = new RegExp(/abc/, g)
var reg4 = new RegExp(/abc/g, i) // 忽略正则表示的修饰符,使用第二个参数作为修饰符。等价于 var reg4 = /abc/i
元字符
字符 | 释义
- | - \d | 匹配一个数字 \D | 匹配一个非数字字符 \w | 匹配一个单字字符(字母、数字或下划线) \W | 匹配一个非单字字符 \s | 匹配一个空格字符(空格、制表符、换页符和换行符) \S | 匹配一个非空格字符 \b | 单词边界 . | 匹配除换行符外的任意单个字符 通过观察,我们可以看出大写表示小写的反义,所以组合/\d\D/、/\w\W/、/\s\S/可以匹配任意字符。
来看一个例子,假如我们想要匹配三个连续单字,我们首先会想到的是/\w\w\w/
var str = 'bad very apple bear eat module'
var reg = /\w\w\w/g
str.match(reg)
// ["bad", "ver", "app", "bea", "eat", "mod", "ule"]
正则表达式实际上匹配一个连续串的规则,因此能够匹配到3个字母的单词,也可以匹配6个字母的单词(module)。
那么这时候我们想要的是匹配三个字母组成的单词呢?这时候需要用到匹配单词边界\b
。
// 修改正则表达式
var reg = /\b\w\w\w\b/g
// 结果
// ["bad", "eat"]
从这里我们可以看出,单词边界指的是一个词不被前或后的另一个字符"跟随"的位置,比如字母和空格之间。而匹配的结果不包含字边界,意味着其长度为0。例如/\ba/可以匹配"apple"的"a",/pp\b/不匹配"apple"的"pp",因为pp后跟随着le。
这里我们可以具体把\b
看作是\w
和\W
之间的位置,还包括与首尾之间的位置。
var str = 'everglow_01.mp4'
var result = str.replace(/\b/g, #)
console.log(result)
// #everglow_01#.#mp4#
转义
正则表达式中可以通过\
来转义,还原字符本身的含义。例如我们知道.
可以匹配除换行符之外的任意字符,当我们想要匹配.
本身的时候,我们可以通过\.
来还原.
本身作为一个点的含义。
如果在非特殊含义的字符前加上\
不会有影响。
字符组
当我们想要匹配一个不确定的字符时,我们需要通过字符组来匹配。通过使用[]
方括号,其中列出所有的可选项,它们之间关系为"或",最终匹配出一个字符。
例如,我们通过/[ab]c/
可以匹配ab
与ac
。方括号中匹配的内容为a或b。
转义\
特殊字符在[]
内不需要进行转义,即[.]
表示匹配一个点。
连字符 -
通常我们使用字符组的时候是需要针对一个范围进行匹配,连字符-
的意思是匹配的范围是它左边和右边之内的值。
var str = 'b3 46 c5'
var reg = /[a-z]\d/g
str.match(reg)
// ["b3", "c5"]
上述的正则表达式,表示匹配一个字母加一个数字的组合,其中字母的范围是从a到z。
我们还可以同时匹配多个范围,例如/[a-zA-Z]/
,这样表示匹配小写的a-z与大写的A-Z范围内的字母。
如果单独使用,或者不符合一个范围使用,则仍然表示匹配一个-
。例如/[-c]/
表示匹配-
或者c
。
取反 ^
在[]
使用^
表示取反。
var str = 'b2 a2 c9'
var reg = /[^ab]\d/g
str.match(reg)
// ["c9"]
可以看出,取反的意义是匹配除a和b之外的字符。
位置(边界)
字符 | 释义 |
---|---|
^ | 匹配文本的开始 |
$ | 匹配文本的结束 |
上面提到在[]
内使用^
表示中括号内的取反,正常使用的^
,则表示匹配输入文本的开始。
var str = 'real react'
var reg = /^r\w+/g
str.match(reg)
// ["real"]
这里只会匹配到real
,因为^
匹配的是文本的开头。注意与单词匹配的区分,如果想要匹配 r 开头的单词,可以使用单词边界:/\br\w+\b/
。
量词
字符 | 释义 |
---|---|
? | 匹配前面一个表达式0次或者1次 |
- | 匹配前面一个表达式1次或者多次
- | 匹配前面一个表达式0次或者多次 {min,max} 匹配前面一个表达式从min到max之间的次数,max为空时表示匹配至少min次 {n} | 匹配前面一个表达式n次
这里的次数表示重复前一个表达式的次数。这里表达式指是一个集合,这个集合可以是单个字符,也可以由若干个字符组成,这个后面会提到。
var str = 'apppple bear'
var reg = /ap{1,4}le/g
str.match(reg)
// ["apppple"]
捕获分组
通过上面量词的例子,我们想要匹配多次出现的p,我们可以使用/p{1,4}/
。那么这时候我们如果想要匹配多次出现的字符组合呢?这个时候我们需要用到正则表达式中的分组()
。
var str = 'appappapp'
var reg = /(app){3}/g
str.match(reg)
// ["appappapp"]
我们通过使用了分组,将app进行包裹,此时可配合量词达到匹配字符组合的目的。
或
上面提到,我们想要表示“或”的关系可以使用字符组[]
。同样地,如果我们想要表示几个字符组合之间的"或"关系时,我们需要用到()
。
var str = 'react really'
var reg = /\brea(ct|lly)/g
str.match(reg)
// ["react", "really"]
引用
我们在使用()
时,实际上产生了一个又一个的捕获分组。而我们可以通过多种方法来获取到某些具体的分组。
$
var str = '123-456-7890'
var reg = /(\d{3})-(\d{3})-(\d{4})/
str.match(reg)
// ["123-456-7890", "123", "456", "7890", index: 0, input: "123-456-7890", groups: undefined]
RegExp.$1
// "123"
通过match方法返回结果来看,除了返回匹配文本自身外,还返回了括号内的捕获分组。并且,我们还能够通过RegExp.$1
显式得去获取某个具体的分组。
在JavaScript中,正则匹配成功的字符串可以用1表示第一个匹配成功的,
$2`表示第二个匹配成功的,以此类推。
同时,也可以使用构造函数的全局属性$1
至$9
来获取,$_
表示匹配文本。
我们可以在上面例子的基础上,配合replace方法使用。
var result = str.replace(reg, '$3/$2/$1')
console.log(result)
// 7890/456/123
\n
通过使用$n
的形式获取分组,我们可以在正则表达式外,使用捕获分组的引用。在正则表达式内,我们可以通过\
+ 数字 的形式,它返回最后的第n个子捕获匹配的子字符串。
var str = '@@react@@'
var reg1 = /@@.*@@/
var reg2 = /(@@).*(\1)/
str.match(reg1) // ["@@react@@", index: 0, input: "@@react@@", groups: undefined]
str.match(reg2) // ["@@react@@", "@@", "@@", index: 0, input: "@@react@@", groups: undefined]
上述的例子中,reg2中的(@@)
表示第一个分组,\1
表示引用第一个分组,即@@
,因此reg1与reg2是等价的。
当捕获分组不存在时,例如以上例子不存在第四个分组,那么这时候是进行了\
的转义,表示对4
这个数字的转义。
如果存在括号嵌套的情况下,我们可以通过判断开括号(
的位置来确定分组顺序。
捕获命名
ES2018中,引入了具名组匹配,允许为每一个分组指定一个名字,这样既利于阅读,也方便引用。具体的方法是在括号内最前面添加?<key>
,其中,key
为我们想要命名的组名。
var result = '2018-09-10'.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/)
console.log(result)
对比于之前的例子,使用捕获命名后,返回结果的groups属性不再是undefined,而是每个组名与对应的匹配值的键值对。
我们可以通过result.groups.xxx
获取某个分组的的值。如果命名组没有匹配,则会返回undefined
,而这个建名依然会存在于groups中。
忽略分组(非捕获分组)
在某些情况下,我们希望忽略某些分组。这时候我们使用在括号内最前面加上?:
var str = 'Tel:123-456-7890'
var reg = /(?:Tel:)(\d{3})-(\d{3})-(\d{4})/
var result = str.replace(reg, '$1-$2-$3')
console.log(result)
// 123-456-7890
我们可以看出,结果中忽略了(Tel:)
分组。
环视(零宽断言)
零宽断言匹配的是:在这个位置之前或者之后,应该有或者没有什么内容。零宽是指它匹配的是一个位置,其本身是没有宽度的。
肯定先行断言
先行指的是向前看(look-ahead),从文本开头向尾部开始解析。断言的这个位置是为前面的规则服务。
语法:圆括号内最前面加上?=
var str = 'JavaScript typescript'
var reg = /\b\w{4}(?=Script\b)/g
str.match(reg)
// ["Java"]
上述的代码中,我们想要匹配的是四个字符,条件是这四个字符必须是一个结尾是Script
的单词,这时候显然命中的是JavaScript
。这时候返回的结果是Java
,而没有返回JavaSciprt
,这也正是零宽的特性,它只是去匹配一个位置,而其中的内容不是匹配结果的一部分。
肯定后行断言
后行指的是从后想钱看(look-behind),与先行相反。
语法:圆括号内最前面加上?<=
var str = 'fruitApple softwareAdobe'
var reg = /(?<=\bfruit)A\w+\b/g
str.match(reg)
// ["Java"]
上述代码中,我们想要匹配词组中A开头到词组的结尾的部分,条件是这个词组是以fruit开头,这时候按后行的规则,从尾部往开头解析,命中了fruitApple
。
否定先行断言
否定的含义与上述肯定相反,相当于一个取反的意义。
语法:圆括号最前面加上?!
var str = 'JavaScript typescript'
var reg = /\b\w{4}(?!Script\b)/g
str.match(reg)
// ["type"]
否定后行断言
语法:圆括号最前面加上?<!
var str = 'fruitApple softwareAdobe'
var reg = /(?<=\bfruit)A\w+\b/g
str.match(reg)
// ["Adobe"]
通过以上的例子可以发现,否定的语法相当于是把肯定中的=
替换为!
。
整数千位分割符的实现
var num = '1234567'
var reg = /\d{1, 3}(?=(\d{3})+$)/g
num.replace(reg, '$&,')
// 1,234,567
贪婪与惰性
贪婪模式是正则中的默认模式,指的是在既定的规则之下匹配尽可能多的文本。
var str = '<div id="container" class="main"></div>'
var reg = /id=".*"/g
str.match(reg)
// ["id="container" class="main""]
从上述代码可以看出,实际上会匹配到main
后面的"
而不在container
后面的"
停下。在贪婪模式下,正则会去尽可能的多匹配,在规则范围内,匹配越多越好。例如/\w*/
、/\d+/
。
与贪婪所对应的就是惰性,顾名思义,惰性模式下会匹配尽可能少的文本。
假设我们只想要匹配id相关的信息,即匹配到container
后面的"
为止。我们可以使用惰性模式。
var str = '<div id="container" class="main"></div>'
var reg = /id=".*?"/g
str.match(reg)
// ["id="container""]
从上述代码可以看出,通常我们在量词后面加上?
就可以实现惰性匹配,例如/\w*?/
、/\d{2,5}?/
。
我们在上面提到的/(a|b)c/
这种或关系的匹配就是惰性的,当匹配到ac
时,不会再继续匹配。
回溯
我们在学习正则表达式的时候,或多或少会听到"回溯"这个词。回溯与正则引擎有关,正则引擎分为NFA和DFA。
JavaScript使用的是NFA。其匹配过程是吃进一个字符,如果通过,则再次耻辱;如果不通过,则吐出,回到上一个状态。我们知道在正则中会存在不同状态的路径,例如/(ab|ac)d/
,如果一条路径不匹配,则会退回去尝试另一条路;如果通过,则继续吃进字符探索。这个吐出字符、状态的回退过程,就是所谓的"回溯"。
无回溯情况
··· javascript
var str = 'abbbc' var reg = /ab{3}c/g str.match(reg) // ["abbbc"]
···
上述代码的匹配过程是没有触发回溯。从正则表达式的第一个字符a
开始,首先吃进abbbc
的第一个字符a
,二者相同,匹配成功。继续吃进第二个字符b
,仍然匹配,以此类推。由于匹配默认是贪婪的,所以每一步都能够匹配成功,一直到最后一个字符。整个过程中没有出现需要"吐出"的回溯。
触发回溯的匹配
var str = 'aaaab'
var reg = /^(a*)ab/
str.match(reg)
-
匹配的开始,
a*
会去尽可能多的捕获a
-
随着
a*
的一直捕获,最终会遇到字符串最后的一个b
。 -
继续执行正则表达式最后的
ab
进行匹配,而我们的字符串中由于被a*
捕获了所有的a
,此时仅剩下一个b
,那么这个时候显然是无法完成匹配的。 -
此时
a*
从已经捕获的字符中吐出一个a
,此时剩余的字符串为ab
,被捕获的是aaa
。 -
重新执行正则中的
ab
进行匹配,发现与剩余的字符串ab
匹配,匹配过程结束。
通过上述的步骤,展示了贪婪模式下的匹配过程。我们可以看出3,4步由于暂时无法匹配而吐出字符的过程,就是触发了回溯。
触发形式
上面的例子中,我们可以看出,一般的量词能够触发回溯。因为匹配的过程是贪婪的,所以会尽量先去匹配更多的文本,直到暂时无法匹配时,会不停地去尝试吐出字符再匹配,直到匹配完成。
除了贪婪量词之外,惰性模式下也会存在回溯的情况。
var str = '12345'
var reg = /^(\d{1,3}?)(\d{1,3})$/
str.match(reg)
// ["12345", "12", "345", index: 0, input: "12345", groups: undefined]
上述代码中,我们的目标字符串是12345
,第一个分组使用了惰性模式,一开始仅仅匹配了一个1
,第二个分组匹配了234
,到最后一个字符5
发现字符串已经没有剩余,无法进行匹配。这时候就会发生回溯。再给第一个分组塞入2
,这时候才能够完成整体的匹配。
性能
我们不能简单从匹配模式来判断性能的好坏,因为贪婪模式与惰性模式都会存在回溯。当回溯越少的情况下,表示性能越好。
通常情况下,由于使用了不合理的正则写法引起灾难性的悲观回溯,可能会导致CPU满载,JavaScript 主进程忙于进行计算,使页面失去响应的情况。例如使用了嵌套量词/(a*)*b/
或是前后重复/a*a*/
等形式会导致这一情况。
修饰符
g修饰符
全局匹配,单词是global。默认情况下,正则从左向右匹配,只要匹配到了结果就会收工。g修饰符会开启全局匹配模式,找到所有匹配的结果。
var str = 'react really'
var reg1 = /\brea\w+\b/
var reg2 = /\brea\w+\b/g
str.match(reg1) // ["react", index: 0, input: "react really", groups: undefined]
str.match(reg2) // ["react", "really"]
i修饰符
忽略大小写,单词是ignoreCase。通过该修饰符,在匹配时可以忽略字母的大小写。
'JavaScript'.match(/javascript/)
// null
'JavaScript'.match(/javascript/i)
// ["JavaScript", index: 0, input: "JavaScript", groups: undefined]
m修饰符
多行匹配,单词是multiline。通常用于搭配^
与$
。默认情况下^
与$
用于匹配文本的开始与结束,加上该修饰符后,变成了匹配多行文本的开始与结束。
`
abc
xyz
`.match(/^xyz$/);
// null
`
abc
xyz
`.match(/^xyz$/m);
// ["xyz", index: 5, input: "↵abc↵xyz↵", groups: undefined]
y修饰符
粘连,单词是sticky。其同样也是全局匹配,与g不同的是,它的后一次匹配都是从上一次匹配成功的位置开始。而g则是剩余位置中存在匹配项即可。
var s = 'aaa_aa_a';
var r1 = /a+/g;
var r2 = /a+/y;
r1.exec(s) // ["aaa"]
r2.exec(s) // ["aaa"]
r1.exec(s) // ["aa"]
r2.exec(s) // null
在第二次匹配时,由于y修饰符有位置要求,所以返回了null。
s修饰符
我们知道/./
表示匹配除换行符之外的任意字符。当我们使用了s修饰符后,便可以通过.
来匹配到包含换行符的任意字符。
u修饰符
含义为“Unicode 模式”,用来正确处理大于\uFFFF的 Unicode 字符。也就是说,会正确处理四个字节的 UTF-16 编码。由于ES5不支持四个字节的 UTF-16 编码,会将其识别为两个字符,使用u修饰符后,便会识别其为一个字符。
配合JS方法应用
RegExp对象常用的方法
test
RegExp.test(string)
test方法用于判断字符串中是否存在匹配正则表达式模式的字符串,若存在返回true,否则返回false。
var str = 'hello world'
var reg = /hello/
var result = reg.test(str)
console.log(result) // true
exec
RegExp.exec(string)
exec方法在非全局调用时,返回第一个匹配的结果,匹配结果和分组以数组的形式返。
var str = '123-456 456-789'
var reg = /(\d{3})-(\d{3})/
reg.exec(str)
// ["123-456", "123", "456", index: 0, input: "123-456 456-789", groups: undefined]
其中,index是匹配文本的第一个字符的位置,input存放的是被检索的字符串str。如果我们想要返回全部匹配的对象,我们可以使用g修饰符。
var str = '123-456 456-789'
var reg = /(\d{3})-(\d{3})/g
reg.exec(str)
// ["123-456", "123", "456", index: 0, input: "123-456 456-789", groups: undefined]
reg.exec(str)
// ["456-789", "456", "789", index: 8, input: "123-456 456-789", groups: undefined]
reg.exec(str)
// null
从上述代码中看出,我们可以通过反复调用exec()方法来便利字符串中所有匹配的文本,当再也找到匹配文本的时候,便返回null。
支持正则表达式的 String 对象的方法
match
string.match(RegExp)
match方法用于检索字符串是否有符合RegExp匹配的文本,与exec方法类似。
在用作非全局调用的情况下,match方法只匹配一次,如果没有找到文本,则返回null;否则,返回一个数组,结构与exec方法返回结果类似。
var str = '123-456 456-789'
var reg = /(\d{3})-(\d{3})/
str.match(reg)
// ["123-456", "123", "456", index: 0, input: "123-456 456-789", groups: undefined]
match方法与exec方法的区别在于,使用g修饰符的情况下,如果匹配成功,match会以数组的形式返回所有的匹配结果,否则返回null。
var str = '123-456 456-789'
var reg = /(\d{3})-(\d{3})/g
str.match(reg)
// ["123-456", "456-789"]
split
string.split(str|RegExp)
split方法我们通常用于将字符串分割为数组,在某些复杂的情况下,我们可以使用正则表达式来帮助我们分割。
var str = 'a1b2c3d'
var reg = /\d/
var result = str.split(reg)
console.log(result)
// ["a", "b", "c", "d"]
通过上面的代码,我们实现了以数字来分割字符串。
replace
str.replace(RegExp|string, newString|function)
replace方法用于将字符串中的符合条件的若干字符替换为新的字符。其第一个参数除了字符串之后,还接受正则表达式,第二个参数可以是一个函数。
var str = 'Apples and apples are round'
var result = str.replace(/apple/ig, 'oranges')
console.log(result)
// orangess and orangess are round
第一个参数为正则表达式的情况下,第二个参数可以传入一个函数,其返回值作为替换字符串。如果我们使用了g修饰符,那么该函数会被调用多次。我们首先来看这个函数的参数:
变量 | 含义
- | - match | 匹配的子字符串 p1, p2, p3... | 表示第n个捕获分组,如果没有分组,则该参数不存在 offset | 匹配的子字符串在原字符串中的位置 string | 被匹配的原字符串 string | 被匹配的原字符串 NamedCaptureGroup | 命名捕获组匹配的对象
我们来看一个例子,我们想要将字符串中的@@...@@
替换为<div>...</div>
。
var str = '@@apple@@and@@bear@@'
var result = str.replace(/(@{2})(\w+?)\1/g, function(match, p1, p2) {
return `<div>${p2}</div>`
})
console.log(result)
// <div>apple</div>and<div>bear</div>