正则表达式,也称规则表达式,经常使用其来完成对字符串的校验和过滤。由于正则表达式的灵活性、逻辑性和功能性都非常强大,而且 可以利用很简单的方式完成对复杂字符串的控制,所以很多程序语言都支持正则表达式。在JavaScript
中正则表示也非常强大和实用。
基本形式
正则表达式(regular expression)是一种表达文本模式(即字符串结构)的方法,有点像字符串的模板,常常用作按照“给定模式”匹配文本的工具。比如,正则表达式给出一个Email地址的模式,然后用它来确定一个字符串是否为Email地址。JavaScript的正则表达式体系是参照Perl 5建立的。
新建正则表达式有两种方法。一种是使用字面量,以斜杠表示开始和结束。
// 字面量形式
var telRegex1 = /^1[3|5|7|8]\d{9}$/;
// 构造函数形式
var telRegex2 = new RegExp('^1[3|5|7|8]\\d{9}$');
以上都是创建了一个内容为^1[3|5|7|8]\d{9}$
的正则表达式,其表示对一个手机号码的校验。必须以1开始,第二位为3/5/7/8,之后为9位数字。
这两种写法——字面量和构造函数——在运行时有一个细微的区别。采用字面量的写法,正则对象在代码载入时(即编译时)生成;采用构造函数的方法,正则对象在代码运行时生成。考虑到书写的便利和直观,实际应用中,基本上都采用字面量的写法。
有一点需要注意,使用构造函数创建正则表达式时,传入的参数是字符串形式的,在字符串内部,\
本身也是一个转义符,因此需要再使用一个\
来对其进行正则表达式的转义。上面第二个示例中,\\d
才能代表任意数字。
关于正则表达式中,各种符号的含义,以及使用方法,请看后面的介绍:
元字符
一些常用的元字符如下:
.
匹配除换行符之外的任意字符\w
匹配字母或数字或下划线或汉字\s
匹配任意的空白符\d
匹配数字\b
匹配单词的开始或结束^
匹配字符串的开始处$
匹配字符串的结束处。*
匹配前面的子表达式任意次。?
匹配前面子表达式0次或一次,等价于{0, 1}
。+
匹配之前子表达式一次到多次,等价于{1, }
。{n}
匹配之前的子表达式n次。{m,n}
匹配之前的子表达式最少m次,最多n次。{n, }
匹配之前的子表达式至少n次。[xyz]
字符集合,表示其中任意一个字符。表示范围可用-
链接,例如[a-z]
表示a-z之间的任意一个字母。还可以这样书写[A-Za-z0-9]
。[^xyz]
字符即可,表示非其中任意一个字符。表示范围可用-
链接,例如[^a-z]
表示非 a-z之间的任意一个字母。|
表示或(or)
关系,例如com|cn
,表示匹配com或者cn。()
用于分组,其分组中的内容可已通过$1-$9
按顺序获取(字符串相关方法中),之后的正则中也可以通过\1-\9
进行引用(正则表达式内部)。(分组0表示整个正则匹配内容或整个正则表达式)
在正则表达式中,以上这些以及一些未列出的元字符都是有自身含义的,如果我们需要匹配这些元字符本身,可以使用
\
对其进行转义即可。
更多元字符可以查看:正则表达式
属性
修饰符
ignoreCase
:返回一个布尔值,表示是否设置了i修饰符,该属性只读。global
:返回一个布尔值,表示是否设置了g修饰符,该属性只读。multiline
:返回一个布尔值,表示是否设置了m修饰符,该属性只读。sticky
:ES6返回一个布尔值,表示是否设置了y修饰符,只读。
var r = /abc/igm;
r.ignoreCase; // true
r.global; // true
r.multiline; // true
匹配时属性
lastIndex
:返回下一次开始搜索的位置。该属性可读写,但是只在设置了g修饰符时有意义。source
:ES5返回正则表达式的字符串形式(不包括反斜杠),该属性只读。flags
:ES6返回正则表达式中的修饰符。
var r = /abc/igm;
r.lastIndex; // 0
r.source; // "abc"
r.flags; //"igm"
方法
test()
正则对象的test
对象接收一个字符串,表示测试字符串,返回一个布尔值,表示是此字符串是否满足匹配条件。
telRegex1.test('13612341234'); // true
telRegex2.test('13612341234'); // true
telRegex1.test('136123412'); // false
如果正则表达式带有g
修饰符,则每一次test
方法都从上一次结束的位置开始向后匹配。同时,可以通过正则对象的lastIndex
属性指定开始搜索的位置。
var xReg = /x/g;
var str = 'xyz_x1_y1_x3';
xReg.lastIndex; // 0
xReg.test(str); // true
xReg.lastIndex; // 1
xReg.test(str); // true
xReg.lastIndex; // 5
// 指定位置开始 指定下次匹配从最后一位开始,就匹配不到了
xReg.lastIndex = 11; // 11
xReg.test(str); // false
xReg.lastIndex; // 0
var indexReg = /^(?:http|https).+\/jwebui\/pages\/themes\/(\w+)\/\1\.jspx(\?\S+)?$/i ;
上面是一个F8中检查是否为首页的正则表达式。
最开始的
^
和最后的$
分别表示匹配的开始和结束。(?:http|https)
表示两者之一,这么写是非获取的组匹配,()
不会被分组存储。也可以写成(http|https)
但是后面的\1
就需要替换成\2
了,因为这么写时此处形成了第一个分组。.+
就是任意字符至少出现一次。\/jwebui\/pages\/themes\/
就是匹配字符串"/jwebui/pages/themes/"
。(\w+)
作为第一个分组,表示任意字母或数字或下划线或汉字至少出现一次。\1
表示对第一个分组的引用,再重复第一分组的内容 。\.jspx
表示.jspx
。-
(\?\S+)?
表示(\?\S+)
匹配的内容出现0次或一次。其中:\?
表示?
。\S+
表示任意可见字符出现至少一次。
`
exec()
正则对象的exec
方法,可以返回匹配结果。如果发现匹配,就返回一个数组,成员是每一个匹配成功的子字符串,否则返回null
。
如果正则表示式包含圆括号(即含有“组匹配”),则返回的数组会包括多个成员。第一个成员是整个匹配成功的结果,后面的成员就是圆括号对应的匹配成功的组。也就是说,第二个成员对应第一个括号,第三个成员对应第二个括号,以此类推。整个数组的length
属性等于组匹配的数量再加1。
var ipReg = /(\d{1,3}\.){3}(\d{1,3})/;
var ipStr = 'My ip is "192.168.118.47" , please tell me yours';
ipReg.exec(ipStr); // ["192.168.118.47", "118.", "47"]
上面第一段代码表示一个简单的IP检验,数字的1-3位之后紧跟一个.
,接着这个整体要出现3次,最后再有一段数字的1-3位。结果数组中,第一个值表示匹配到的结果,之后的表示正则分组匹配到的内容。
如果正则表达式加上g修饰符,则可以使用多次exec方法,下一次搜索的位置从上一次匹配成功结束的位置开始。同时还可以指定lastIndex
,使之下次从指定位置开始(可见之前的test
示例)。
var ipLastReg = /\d+(?=;)/g;
var ipsStr = '192.168.118.47;192.168.118.46;192.168.118.48;';
ipLastReg.exec(ipsStr); // ["47"]
ipLastReg.exec(ipsStr); // ["46"]
ipLastReg.exec(ipsStr); // ["48"]
上面代码中正则中的(?=;)
表示先行断言,表示只匹配在;
前面\d+
。
如果只是为了得到是否匹配,请使用
RegExp.test()
方法或字符串实例的.search()
替代,效率更高。
字符串相关方法
之所以称之为字符串相关方法是因为其是在字符串上调用的(虽然ES6开始,内部调用的是正则上的方法,但还是在字符串上提供的入口)。
match()
:返回一个数组,成员是所有匹配的子字符串。search()
:按照给定的正则表达式进行搜索,返回一个整数,表示匹配开始的位置。replace()
:按照给定的正则表达式进行替换,返回替换后的字符串。split()
:按照给定规则进行字符串分割,返回一个数组,包含分割后的各个成员。
match()
match
方法对字符串进行正则匹配,返回匹配结果。此方法方法与正则对象的exec
方法非常类似:匹配成功返回一个数组,匹配失败返回null
。如果正则表达式带有g
修饰符,则该方法与正则对象的exec
方法行为不同,会一次性返回所有匹配成功的结果。
var ipLastReg = /\d+(?=;)/g;
var ipsStr = '192.168.118.47;192.168.118.46;192.168.118.48;';
ipsStr.match(ipLastReg); // ["47", "46", "48"]
上面的正则是匹配IP中的最后一位,其中使用了(?=;)
意为先行断言,表示只匹配在;
之前的内容,但是不包括;
。关于更多先行断言,请看下文。
search()
search
方法,返回第一个满足条件的匹配结果(可直接使用字符串,不一定是正则对象)在整个字符串中的位置。如果没有任何匹配,则返回-1
。
var nowDateStr = '2016-11-1';
var testReg = /-/g;
nowDateStr.search(testReg); // 4
// 再次查找还是4
nowDateStr.search(testReg); // 4
// 检查lastIndex 并设置
testReg.lastIndex; // 0
testReg.lastIndex = 6;
nowDateStr.search(testReg); // 4 结果仍为4
search
方法总是从字符串的开始位置查找,与正则表达式的g
修饰符和lastIndex
属性无关。
replace()
replace
方法可以替换匹配的值,返回替换后的新字符串。它接受两个参数,第一个是搜索模式(可直接使用字符串,不一定是正则对象),第二个是替换的内容(可使用字符串或一个函数)。搜索模式如果不加g修饰符,就替换第一个匹配成功的值,否则替换所有匹配成功的值。
其中replace
方法的第二个参数可以使用美元符号$,用来指代所替换的内容,具体如下所示:
$&
指代匹配的子字符串。$`
指代匹配结果前面的文本。$'
指代匹配结果后面的文本。$n
指代匹配成功的第n组内容,n是从1开始的自然数。$$
指代美元符号$。
var re = /-/g;
var str = '2016-11-01';
var newstr = str.replace(re,'.');
console.log(newstr); // "2016.11.01"
'hello world'.replace(/(\w+)\s(\w+)/, '$2 $1');
// "world hello"
'abc'.replace('b', '[$`-$&-$\']');
// "a[a-b-c]c"
第二个参数为函数:
function toCamelStyle(str) {
// 匹配-以及之后的一个字符,其中这个字符在一个分组内
var camelRegExp = /-([a-z])/ig;
return str.replace(camelRegExp, function(all, letter) {
// all为匹配到的内容,letter为组匹配
return letter.toUpperCase();
});
}
toCamelStyle('margin-left'); // "marginLeft"
toCamelStyle('aa-bb-cccc'); // "aaBbCccc"
以上代码展示通过正则将aa-bb-cccc
这样的字符串转化为aaBbCccc
这种形式。replace
回调函数接收两个参数,第一个为匹配到的内容,第二个为匹配到的分组,有多少组就可以传多少个参数,在此之后还可以有两个参数,一个为匹配到内容在原字符串的位置,另一个是原字符串。
split()
split
方法按照正则规则分割字符串,返回一个由分割后的各个部分组成的数组。该方法接受两个参数,第一个参数是分隔规则(可直接使用字符串,不一定是正则对象),第二个参数是返回数组的最大成员数。
'2016-11-01'.split('-'); // ["2016", "11", "01"]
'2016-11-01'.split(/-/); // ["2016", "11", "01"]
贪婪模式和懒惰模式
当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符,称之为贪婪模式。
例如:
var s = 'aaa';
s.match(/a+/); // ["aaa"]
有时,我们更需要懒惰匹配,也就是匹配尽可能少的字符。前面给出的限定符都可以被转化为懒惰匹配模式,只要在它后面加上一个问号?。这样.*?就意味着匹配任意数量的重复,但是在能使整个匹配成功的前提下使用最少的重复。
var s = 'aaa';
s.match(/a+?/); // ["a"]
以下是一些说明
*?
重复任意次,但尽可能少重复+?
重复1次或更多次,但尽可能少重复??
重复0次或1次,但尽可能少重复{n,m}?
重复n到m次,但尽可能少重复{n,}?
重复n次以上,但尽可能少重复
也就是说默认情况下,都是贪婪模式,加上一个?
时就转化为了懒惰模式,也称非贪婪模式。
组匹配
通常一个()
中的内容就构成了一个分组,此分组内容将被存储,可在之后的正则表达式(使用\1-\9
)和相关方法中(使用 $1-$9
)引用,前面已经介绍过了,就不再说了。
关于组匹配,还有以下几种情况:
非捕获组
(?:x)
称为非捕获组(Non-capturing group),表示不返回该组匹配的内容,即匹配的结果中不计入这个括号。
// 正常匹配
var url = /(http|ftp):\/\/([^/\r\n]+)(\/[^\r\n]*)?/;
url.exec('http://google.com/');
// ["http://google.com/", "http", "google.com", "/"]
// 非捕获组匹配
var url = /(?:http|ftp):\/\/([^/\r\n]+)(\/[^\r\n]*)?/;
url.exec('http://google.com/');
// ["http://google.com/", "google.com", "/"]
之后先行断言和先行否定断言也都是非捕获组
先行断言
x(?=y)
称为先行断言(Positive look-ahead),x
只有在y
前面才匹配,y
不会被计入返回结果。
比如之前匹配ip的例子:
var ipLastReg = /\d+(?=;)/g;
var ipsStr = '192.168.118.47;192.168.118.46;192.168.118.48;';
ipsStr.match(ipLastReg); // ["47", "46", "48"]
上面正则对象中(?=;)
就表示只匹配在;
之前的内容,但是不包括;
。
先行否定断言
x(?!y)
称为先行否定断言(Negative look-ahead),x
只有不在y
前面才匹配,y
不会被计入返回结果。
var xreg = /\d+(?!%)/g ;
xreg.exec('100% is 1'); // ["10"]
xreg.exec('100% is 1'); // ["1"]
/\d+?(?!%)/.exec('100% is 1'); // ["1"]
上面代码表示匹配不在%
前的数字,xreg
中直接书写的\d+
表示贪婪模式,因此第一次匹配到的是10,第二次才会匹配到后面的1,因为作为数字10本身也不在%
前面,正则不会将100当成一个整体(注意:这里需要定义一个正则对象来调用,直接以字面量形式的正则调用时,每次调用都是一个新对象,结果始终是10
)。
为了一次匹配到最后的1
,我们在\d+
之后加一个?
将其转为非贪婪模式即可。
为了一次匹配到前面100
中的1
,我们在\d+
之后加一个?
将其转为非贪婪模式即可。
ES6之前,
JavaScript
中不支持后行断言和否定后行断言,ES6中添加了对此的支持,请看之后的ES扩展部分。
ES6扩展
构造函数
RegExp构造函数的参数有两种情况。
第一种情况是,参数是字符串,这时第二个参数表示正则表达式的修饰符(flag)。
第二种情况是,参数是一个正则表示式,此时不能有第二个参数,会返回一个原有正则表达式的拷贝。
ES6 针对第二种情况,允许传入第二个参数,用于设置第一个参数正则表达式的修饰符。
var regex = new RegExp(/xyz/, 'i'); // ES6之前 语法错误
new RegExp(/abc/ig, 'i'); // ES6中结果为: /abc/i
字符串的正则方法
字符串对象共有4个方法,可以使用正则表达式:match()
、replace()
、search()
和split()
。
ES6将这4个方法,在语言内部全部调用RegExp
的实例方法,从而做到所有与正则相关的方法,全都定义在RegExp对象上。
修饰符
ES6对正则表达式添加了u
修饰符,含义为“Unicode
模式”,用来正确处理大于uFFFF的Unicode字符。也就是说,会正确处理四个字节的UTF-16编码。
ES6还为正则表达式添加了y
修饰符,叫做“粘连”(sticky
)修饰符。
y
修饰符的作用与g
修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g
修饰符只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。
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
是类似于在每次匹配时隐式地添加了^
,表示开始位置。
属性
ES5中,正则对象存在source
属性,用于返回正则表达式本身。
ES6中,又添加了flags
属性,用于返回正则对象的所有修饰符。
后行断言
后行断言于先行断言相反。例如/(?<=y)x/
表示匹配x
,但是要求x
必须在y
后面。
同理 后行否定断言则为:/(? 表示匹配
x
,但是要求x
不能在y
后面。
需要注意的是,存在后行断言时,正则执行顺序发生了改变,会先匹配后行断言的这部分,再匹配其他的的,顺序变成了从右向左。因此一些匹配操作的结果可能大不一致,而且正则中的
\1-\9
的引用顺序也会发生变化。
参考链接
ES6入门 - 正则表达式
JavaScript RegExp
正则表达式30分钟入门教程
原文发表在我的博客JavaScript正则表达式RegExp,欢迎访问!
错误修正
先行否定断言中
为了一次匹配到最后的1
,我们在\d+
之后加一个?
将其转为非贪婪模式即可。
为了一次匹配到前面100
中的1
,我们在\d+
之后加一个?
将其转为非贪婪模式即可。