正则表达式是一个精巧的利器,经常用来在字符串中查找和替换,JavaScript语言参照Perl,也提供了正则表达式相关模块,开发当中非常实用,在一些类库或是框架中,比如jQuery,就存在大量的正则表达式,所以说学好正则表达式,是提高开发技能的一项基本要求。那么今天博主就来详细总结一下正则表达式的相关知识,希望不熟悉的同学们,也能够掌握正则表达式的原理及应用。
在JS中,创建正则表达式有两种方式,一种是字面量方式,一种是构造器方式,如下所示:
var regex = /\w+/;
// 或者
var regex = new RegExp('\\w+');大家也许注意到,使用字面量要比构造器简洁得多,\w表示一个word,匹配单个字母、数字或下划线,而使用RegExp构造器时,我们的正则变为了"\\w",这是因为要在字符串中表示一个反斜杠\,我们需要对其转义,也就是在前面再加一个转义字符\。相信大家都知道,要在字面量正则中表达一个匹配反斜杠\的正则,只需写成\\这样,但在字符串中表达这个正则,则是"\\\\"这个样子的,这是因为字符串中前两个表示一个反斜杠\,后两个也表示一个反斜杠\,最终在正则层面,结果还是\\。
对于上面两种创建形式,都可以加上一些后缀修饰符,这些修饰符可以单个使用,也可以组合起来使用:
/\w+/g; // global search
/\w+/i; // ignore case
/\w+/m; // multi-line
/\w+/u; // unicode
/\w+/y; // sticky
/\w+/gi;
new RegExp('\\w+', 'gi');从英文注释来看,相信大家都大概都略知一二了,需要注意的是u和y修饰符,它们是ES6新增的特性,u表示启用Unicode模式,对于匹配中文特别有用,而y是sticky,“粘连”的意思,表示下次匹配时目标紧随当前匹配项,这个我们后面会介绍。
正则相关方法
有了正则表达式对象了,如何使用呢?JS中的正则和字符串在原型中均提供相应的方法,先来看看正则原型中的两个方法:
RegExp.prototype.test(str);
RegExp.prototype.exec(str);上面的test()和exec()方法都需传入一个字符串,对这个字符串进行搜索和匹配,不同的是,test()方法会返回true或false,表示字符串和正则是否匹配,而exec()方法在匹配时返回一个匹配结果数组,如果不匹配,则只返回一个null值,下面来看看两者的差异:
// RegExp#test()
var regex = /hello/;
var result = regex.test('hello world'); // true
// RegExp#exec()
var regex = /hello/;
var result = regex.exec('hello world'); // ['hello']对于exec()方法,如果正则中含有捕获组,匹配后则会出现在结果数组中:
// (llo)是一个捕获组
var regex = /he(llo)/;
var result = regex.exec('hello world'); // ['hello', 'llo']开发当中,test()方法一般用于用户输入验证,比如邮箱验证,手机号验证等等,而exec()方法一般用于从特定内容中获取有价值的信息,比如从用户邮箱输入中获取其ID和邮箱类型,从手机号中获取此号码的归属地等等。
字符串相关方法
上面是正则原型中的两个方法,现在来看看字符串原型中都提供了哪些可用的方法:
String.prototype.search(regexp);
String.prototype.match(regexp);
String.prototype.split([separator[, limit]]);
String.prototype.replace(regexp|substr, newSubStr|function);先来说说String#search()方法,它会根据正则参数对字符串进行匹配搜索,如果匹配成功,就返回第一次匹配处的索引,如果匹配失败,则返回-1。
// String#search()
'hello world'.search(/hello/); // 0
'hello world'.search(/hi/); // -1String#match()方法跟RegExp#exec()方法相似,会返回结果数组,所不同的是,如果String#match()的正则参数中含有全局标记g,则结果中会只出现匹配的子串,而忽略捕获组,这一点与RegExp#exec()有些出入。且看下面代码:
// String#match()
'hello hello'.match(/he(llo)/); // ['hello', 'llo']
// String#match()遇到全局g修饰符时会舍弃捕获组
'hello hello'.match(/he(llo)/g); // ['hello', 'hello']
// RegExp#exec()仍旧包含捕获组
/he(llo)/g.exec('hello hello'); // ['hello', 'llo']所以,如果需要总是将捕获组作为结果返回,应该使用RegExp#exec()方法,而不是String#match()方法。
接下来说说String#split()方法,这个方法用于将字符串分割,然后返回一个包含其子串的数组结果,其中separator和limit参数都是可选的,separator可指定为字符串或正则,limit指定返回结果个数的最大限制。如果separator省略,该方法的数组结果中仅包含自身源字符串;如果sparator指定一个空字符串,则源字符串将被以字符为单位进行分割;如果separator是非空字符串或正则表达式,则该方法会以此参数为单位对源字符串进行分割处理。下面代码演示了该方法的使用:
// String#split()
'hello'.split(); // ["hello"]
'hello'.split(''); // ["h", "e", "l", "l", "o"]
'hello'.split('', 3); // ["h", "e", "l"]
// 指定一个非空字符串
var source = 'hello world';
var result = source.split(' '); // ["hello", "world"]
// 或者使用正则表达式
var result = source.split(/\s/); // ["hello", "world"]如果separtor是一个正则表达式,并且正则中包含捕获组,则捕获组也会出现在结果数组中:
// String#split() 正则捕获组
var source = 'matchandsplit';
var result = source.split('and'); // ["match", "split"]
var result = source.split(/and/); // ["match", "split"]
// 正则中含捕获组
var result = source.split(/(and)/); // ["match", "and", "split"]最后来介绍一下String#replace()方法,它会同时执行查找和替换两个操作。
从上面的函数签名来看,该方法会接受两个参数:第一个参数可以是一个正则表达式,也可以是一个字符串,它们都表示将要匹配的子串;第二个参数可以指定一个字符串或是一个函数,如果指定一个字符串,表示这个字符串将会替换掉已匹配到的子串,如果指定一个函数,则函数的返回值会替换掉已匹配的子串。
String#replace()方法最终会返回一个新的已经过替换的字符串。下面分别演示了replace方法的使用:
// String#replace()
var source = 'matchandsplitandreplace';
var result = source.replace('and', '-'); // "match-splitandreplace"
// 或者
var result = source.replace(/and/, function() {
return '-';
}); // "match-splitandreplace"从上面的代码中可以看到,'and'被替换成了'-',但我们同时也注意到,只有第一个'and'被替换了,后面的并没有被处理。这里我们就需要了解,String#replace()方法只对第一次出现的匹配串进行替换,如果我们需要全局替换,需要将第一个参数指定为正则表达式,并追加全局g修饰符,就像下面这样:
// String#replace() 全局替换
var source = 'matchandsplitandreplace';
var result = source.replace(/and/g, '-'); // "match-split-replace"
var result = source.replace(/and/g, function() {
return '-';
}); // "match-split-replace"初学者看到上面的代码,可能会觉得疑惑,对于第二个参数,直接指定一个字符串也挺简单的嘛,我们为何要使用一个函数然后再返回一个值呢。我们看看下面的例子就知道了:
// String#replace() 替换函数的参数列表
var source = 'matchandsplitandreplace';
var result = source.replace(/(a(nd))/g, function(match, p1, p2, offset, string) {
console.group('match:');
console.log(match, p1, p2, offset, string);
console.groupEnd();
return '-';
}); // "match-split-replace"上面代码中,第一个参数是正则表达式,其中包含了两个捕获组(and)和(nd),第二个参数指定一个匿名函数,其函数列表中有一些参数:match, p1, p2, offset, string,分别对应匹配到的子串、第一个捕获组、第二个捕获组、匹配子串在源字符串中的索引、源字符串,我们可以称这个匿名函数为“replacer”或“替换函数”,在替换函数的参数列表中,match、offset和string在每一次匹配时总是存在的,而中间的p1、p2等捕获组,String#replace()方法会根据实际匹配情况去填充,当然,我们还可以根据arguments获取到这些参数值。
下面是代码运行后的控制台打印结果:
现在来看,指定一个函数要比指定一个字符串功能强的多,每次匹配都能获取到这些有用的信息,我们可以对其进行一些操作处理,最后再返回一个值,作为要替换的新子串。所以推荐在调用String#replace()方法时,使用上面这种方式。
上面是String类与正则相关的常用方法,需要注意的是,String#search()和String#match()方法签名中参数均为正则对象,如果我们传递了其他类型的参数,会被隐式转换为正则对象,具体的步骤是先调用参数值的toString()方法得到字符串类型的值,然后调用new RegExp(val)得到正则对象:
// -> String#search(new RegExp(val.toString()))
'123 123'.search(1); // 0
'true false'.search(true); // 0
'123 123'.search('\\s'); // 3
var o = {
toString: function() {
return '\\s';
}
};
'123 123'.search(o); // 3
// -> String#match(new RegExp(val.toString()))
'123 123'.match(1); // ["1"]
'true false'.match(true); // ["true"]
'123 123'.match('\\s'); // [" "]
var o = {
toString: function() {
return '1(23)';
}
};
'123 123'.match(o); // ["123", "23"]而split()和replace()方法不会将字符串转为正则表达式对象,对于其他类型值,只会调用其toString()方法将参数值转为字符串,也不会进一步向正则转换,大家可以亲自测试一下。
以上就是正则的相关基本知识及常用方法,限于篇幅原因,更多关于正则表达式的内容,博主会安排在下一篇中介绍和讲解,敬请期待。
正则修饰符又称为正则标记(flags),它会对正则的匹配规则做限定,进而影响匹配的最终结果。在上次的文章中我们也提到过,正则修饰符一共有以下几种,可以单独使用,也可以组合使用:
/\w+/g; // global search
/\w+/i; // ignore case
/\w+/m; // multi-line
/\w+/u; // unicode
/\w+/y; // sticky
/\w+/gi;
new RegExp('\\w+', 'gi');其中的i好理解,正如上面的注释一样,ignore case或case insensitive,忽略大小写。
下面是一个简单的例子,正则表达式加上了i修饰符之后也可以匹配到大写字母:
'Hello World'.match(/hello/i); // ["Hello"]
/hello/i.exec('Hello World'); // ["Hello"]再来看看全局匹配修饰符g,下面是一个全局匹配的例子:
var source = 'hello world hello JS';
source.match(/hello/); // ["hello"]
source.match(/hello/g); // ["hello", "hello"]从上面代码中可以看出,普通正则的匹配结果只有一个,如果想要找出全部的匹配结果,后面则需要加一个g修饰符,使其成为全局匹配模式。
全局修饰符g通常也会和多行匹配修饰符m结合使用,我们将上面例子稍加改动,添加一个换行符,正则也稍加修改:
var source = 'hello world\nhello JS';
source.match(/^hello.+/g); // ["hello world"]大家会看到,我们是要在多行文本中匹配以"hello"开头的字符串,但结果只出现了第一个匹配项,后面的"hello JS"并未匹配到,这时我们需要加入多行匹配修饰符m:
var source = 'hello world\nhello JS';
source.match(/^hello.+/gm); // ["hello world", "hello JS"]现在,所有的结果都匹配到了。
但需要注意的是,单独使用修饰符m是不起作用的,它必须和g相结合,就像下面例子一样,虽然有m修饰符,但仍旧只匹配到了第一行文字:
var source = 'hello world\nhello JS';
source.match(/^hello.+/m); // ["hello world"]另外,还有一个很重要的条件,那就是,只有正则中包含起始标记"^"或结束标记"$"时,修饰符m才会发挥它的作用,否则g不需要m,且看下面例子:
// 只有匹配开始标记^或结束标记$时,g才需要m
var source = 'hello world\nhey world';
// 正则中没有^或$ 只需g即可匹配多行
source.match(/he.+/g); // ["hello world", "hey world"]
// 正则中含有^或$ g只能匹配第一个结果
source.match(/^he.+/g); // ["hello world"]
source.match(/.+world$/g); // ["hey world"]
// 含有^或$的情况下 需要添加m 才可以匹配多行
source.match(/^he.+/gm); // ["hello world", "hey world"]
source.match(/.+world$/gm); // ["hello world", "hey world"]以上介绍的都是正则修饰符在String#match()方法中的表现,我们也知道,RegExp#exec()是与之对应的一个方法,同样可以匹配字符串,返回结果数组,那么这个exec()方法对于含有全局修饰符的正则又会有什么样的表现呢?实际操作发现,RegExp#exec()方法与上面String#match()的规则大致相同,但不同的是,RegExp#exec()方法每次只会匹配一个结果,所以需多次环执行才能获取全部。我们来看下面示例:
var regex = /^hello.+/gm;
var source = 'hello world\nhello JS';
regex.exec(source); // ["hello world"]
regex.exec(source); // ["hello JS"]可以看到每一次执行正则实例的exec()方法都会返回一个结果数组,由于正则中含有起始标记^和gm组合,我们需要执行两次才能获取到全部的结果,这是与String#match()方法不同的地方。一般来说,我们可以使用循环结构调用RegExp#exec()方法来获取所有的结果:
var result = null;
while (result = regex.exec(source)) {
console.log(result);
}
// output:
// ["hello world"]
// ["hello JS"]对于RegExp#test()方法,一般是用来检测字符串是否匹配某种模式,如果要在多行中检测任意一行是否匹配时,同样需要gm组合,下面代码先简单检测匹配情况,然后在多行中进行匹配:
var source = 'hello world\nhey JS';
/^hello.+/.test(source); // true
/^hey.+/.test(source); // false
/^hey.+/g.test(source); // false
/^hey.+/gm.test(source); // true从结果来看,不加gm修饰符的正则,只能检测一行数据的匹配情况,加入gm后可以对多行进行检测,只要任意一行符合条件,即返回true。
最后再来说说String#replace()方法,同样地,如果正则中出现了^或$,那就需要加上gm组合,下面代码演示了多行替换的操作:
var source = 'hello world\nhello JS';
// 正则中没有^或$,全局g轻松搞定
source.replace(/hello/g, 'hey'); // "hey world\nhey JS"
// 正则中含有^或$,全局g也无能为力,仅能替换第一行
source.replace(/^hello/g, 'hey'); // "hey world\nhello JS"
// 需要使用gm组合
source.replace(/^hello/gm, 'hey'); // "hey world\nhey JS"上面是全局匹配g和多行匹配m,下面介绍一下u修饰符。
u修饰符是ES6新增特性,可以启用Unicode模式对字符串进行正则匹配,能正确处理四个字节的UTF-16字符集。为什么需要这个修饰符呢,我们先来看一个例子:
/^.{3}$/.test('你好啊'); // true
/^.{3}$/.test('𠮷野家'); // false上面正则用于检测字符串是否由三个字符组成,大家看到结果也许会比较困惑,为什么第二个不匹配呢?原因在于“𠮷野家”中的“𠮷”字,仔细观察,会发现它并不是“吉祥”中的“吉”字,而是它的一个异形字,这个字也属于汉字,不过后来在日文中较为常见,对于这样的字,一般的正则很难匹配到,而在ES6中,我们可以指定一个u修饰符,启用Unicode模式,对大于\uFFFF的Unicode字符进行匹配。下面我们添加了u修饰符之后,正则就匹配成功了:
/^.{3}$/u.test('𠮷野家'); // true类似地,还有下面两个例子:
// 下面正则中含有Unicode字符和对应的量词
/𠮷{3}/.test('𠮷𠮷𠮷'); // false
/𠮷{3}/u.test('𠮷𠮷𠮷'); // true
// 下面正则用于匹配非空格字符
/^\S$/.test('𠮷'); // false
/^\S$/u.test('𠮷'); // true另外,ES6新增了大括号Unicode表示法,就像下面这样:
var a = '\u{20BB7}'; // '𠮷'
/\u{20BB7}/u.test('𠮷'); // true不过在使用这种表达方式时,要特别小心,因为\u在普通的正则中表示匹配字符u,而开启Unicode模式时,就不一定了:
// 检测是否含有3个'u'字符
/\u{3}/.test('uuu'); // true
// 检测是否含有字符'\u{3}'
/\u{3}/u.test('uuu'); // false
/\u{3}/u.test('\u{3}'); // true从上面结果来看,开启Unicode模式时,\u和量词{3}组成了一个Unicode字符,所以结果也就完全不同了,在实际开发中,要特别注意。
讲完u修饰符之后,最后再来介绍一下ES6新增的y修饰符。
y修饰符对应的英文全称是sticky,“黏连”的意思,表示后一次的匹配目标必须紧随前一次匹配项,也就是说,多次匹配时,每次的匹配目标必须在起始位置,后一次匹配必须在前一次匹配成功的下一个位置进行。y修饰符与全局g修饰符有些相似之处,但又有区别,先来看下面例子:
var source = 'hello-hello-world';
var re1 = /hello/g;
var re2 = /hello/y;
re1.exec(source); // ["hello"]
re2.exec(source); // ["hello"]
console.log(re2.lastIndex); // 6
// 新一轮匹配将会在"-hello-world"中进行
re1.exec(source); // ["hello"]
re2.exec(source); // null可以看到,re1是g修饰,re2是y修饰,第一轮都相同,然后我们通过正则实例的lastIndex属性获取下一轮匹配的起始位置,进而得知下一轮将会在"-hello-world"中进行匹配,由于它起始位置并不是"hello",其结果是,g仍旧匹配成功,而y匹配失败,返回null。
我们再来稍微修改一下正则,让第二次匹配时,"hello"处于起始位置,使其成功匹配:
var source = 'hello-hello-world';
var re2 = /hello-/y;
re2.exec(source); // ["hello-"]
console.log(re2.lastIndex); // 6
// 这时新一轮匹配将会在"hello-world"中进行
re2.exec(source); // ["hello-"]我们还可以通过修改正则的lastIndex属性,改变新一轮匹配起始位置的索引,进而使其符合y的规则,同样可以匹配成功:
var source = 'hello-hello-world';
var re2 = /hello/y;
re2.exec(source); // ["hello"]
// 手动更改lastIndex
re2.lastIndex = 6;
re2.exec(source); // ["hello"]从上面几个例子来看,y修饰符要求匹配目标必须出现在起始位置,这一点隐含了起始标记"^"的作用:
/hello/y.test('-hello'); // false
// 相当于下面这样
/^hello/.test('-hello'); // false当y和全局g同时出现时,全局匹配将会受限,除了上面讲到的Regex#exec()方法,gy组合还会对String的replace和match两个方法起作用,全局匹配g加上y之后,匹配模式更严格了一些,每一次匹配时目标必须出现在首位:
// String#replace()
'hello-hello-world'.replace(/hello/g, 'hey'); // "hey-hey-world"
// 加上y之后,第二个hello不会被匹配和替换
'hello-hello-world'.replace(/hello/gy, 'hey'); // "hey-hello-world"
// 需要改成下面这样
'hello-hello-world'.replace(/hello-/gy, 'hey-'); // "hey-hey-world"
// String#match()
'hello-hello-world'.match(/hello/g); // ["hello", "hello"]
'hello-hello-world'.match(/hello/gy); // ["hello"]
'hello-hello-world'.match(/hello-/gy); // ["hello-", "hello-"]以上就是正则修饰符的全部内容,在下一篇中博主将会介绍元字符和高级匹配的相关内容,敬请期待。
在上两篇文章中博主介绍了JavaScript中的正则常用方法和正则修饰符,今天准备聊一聊元字符和高级匹配的相关内容。
首先说说元字符,想必大家也都比较熟悉了,JS中的元字符有以下几种:
/ \ | . * + ? ^ $ ( ) [ ] { }它们都表示特殊的含义,下面我们就来一一介绍它们。
/ (slash)
用于创建一个字面量的正则表达式:
var re = /abc/;\ (backslash)
用于对其他字符进行转义,我们称其为转义字符,上面列举的几个元字符,由于它们都表示特殊的含义,如果要匹配这些元字符本身,就需要转义字符的帮忙了,比如我们要匹配一个斜杠 / 的话,就需要像下面这样:
/\//.test('a/b');| (vertical bar)
一般用于两个多选分支中,表示“或”的关系,有了它,我们就能匹配左右两边任意的子表达式了,下面例子匹配单词see或sea:
/see|sea/.test('see'); // true
/see|sea/.test('see'); // true. (dot)
匹配除换行符以外的任意字符,我们可以使用它来匹配几乎所有的字母或字符,除了\r (\u000D carriage return)和\n (\u000A new line),看下面例子:
/./.test('w'); // true
/./.test('$'); // true
/./.test('\r'); // false
/./.test('\n'); // false但需要注意的是,如果遇到码点大于0xFFFF的Unicode字符,就不能识别了,必须加上u修饰符:
/^.$/.test('𠮷'); // false
/^.$/u.test('𠮷'); // true* (asterisk)
用于匹配0到多个子表达式,也就是说,子表达式可有可无,可多可少。如果我们在单个字符后加上星号,它仅作为这个字符的量词,最终的匹配结果还与上下文有关,看下面例子:
/lo*/.test('hell'); // true
/lo*/.test('hello'); // true
/lo*/.test('hellooo'); // true
/lo*/.test('hey yo'); // false
/yo*/.test('hey yo'); // true+ (plus)
用于匹配1到多个子表达式,也就是说,子表达式必须存在,至少连续出现1次。我们还用上面的例子,结果会有所不同:
/lo+/.test('hell'); // false
/lo+/.test('hello'); // true
/lo+/.test('hellooo'); // true? (question mark)
用于匹配0到1个子表达式,也就是说,子表达式要么不存在,要么必须出现一次,不能连续出现多次。我们对上面的例子稍加改动:
/lo?$/.test('hell'); // true
/lo?$/.test('hello'); // true
/lo?$/.test('hellooo'); // false^ (caret) & $ (dollar)
这两个元字符分别用来限定起始和结束,我们在上面的例子中也使用到了,这里再举一个简单的示例:
/^hello/.test('hello'); // true
/world$/.test('world'); // true
/^hello/.test('hey yo'); // false
/world$/.test('word'); // false我想大概很多人最初接触这两个元字符时,都写过这样的程序 - 去除字符串前后多余空格:
var source = ' hello world ';
var result = source.replace(/^\s+|\s+$/g, '');
console.log(result);
// output:
// "hello world"( ) open parenthesis & close parenthesis
用于声明一个捕获组,括号中的子表达式将被匹配并记住,作为捕获组的内容,它们会从索引为1的位置,出现在结果数组中:
/hel(lo)/.exec('hello'); // ["hello", "lo"]
/he(l(lo))/.exec('hello'); // ["hello", "llo", "lo"][ ] open bracket & close bracket
用于声明一个字符集合,来匹配一个字符,这个字符可以是集合中的任意一个,先看下面例子:
/[abc]/.test('b'); // true我们也可以在其中两个字符中间加入一个 - (hyphen) ,用于表示字符的范围,下面例子效果与上面等同:
/[a-c]/.test('b'); // true如果 - 出现在集合的首尾处,则不再表示范围,而是匹配一个实际的字符,如下所示:
/[-a]/.exec('-abc'); // ["-"]
/[c-]/.exec('-abc'); // ["-"]从上面的例子中,我们也可以看到,集合中的字符会按顺序优先匹配。除此之外,多个范围也可同时出现,使整个集合有了更大的匹配范围:
/[A-Za-z0-9_-]/.exec('hello'); // ["h"]其中的"A-Za-z0-9_"可以用"\w"表示,所以下面例子效果与上面等同:
/[\w-]/.exec('hello'); // ["h"]最后,我们还记得上面介绍的^吗,在一般的表达式中,它表示起始标记,但如果出现在[]的起始位置,会表示一个否定,表示不会匹配集合中的字符,而是匹配除集合字符以外的任意一个字符:
/[^abc]/.test('b'); // false
/[^a-c]/.test('b'); // false
/[^a-c]/.test('d'); // true{ } open brace & close brace
作为子表达式的量词,限定其出现的次数,有x{n},x{n,},x{n,m}几种用法,下面分别举例说明:
// o{3} o须出现3次
var re = /hello{3}$/;
re.test('hello'); // false
re.test('hellooo'); // true
// o{1,} o出现次数大于或等于1
var re = /hello{1,}$/;
re.test('hell'); // false
re.test('hello'); // true
re.test('hellooo'); // true
// o{1,3} o出现次数介于1和3之间,包括1和3
var re = /hello{1,3}$/;
re.test('hello'); // true
re.test('helloo'); // true
re.test('hellooo'); // true
re.test('hell'); // false
re.test('hellooooo'); // false另外,我们上面讲到的*,+,?,他们都有与之对应的表示法:
* 0到多次 相当于{0,}
+ 1到多次 相当于{1,}
? 0或1次 相当于{0,1}
说完了上面的几种元字符,再来简单看一下几个常用的转义字符:
元字符部分先到这里,下面我们来聊聊正则高级匹配。
捕获组引用
上面我们也了解到,(x)是一个捕获组,x是捕获组中的子表达式,匹配到的捕获组会从索引为1处出现在结果数组中。这些匹配到的捕获组,我们可以使用$1 ... $n表示:
// $1 ... $n 表示每一个匹配的捕获组
var re = /he(ll(o))/;
re.exec('helloworld'); // ["hello", "llo", "o"]
// "llo" -> $1
// "o" -> $2在String#replace()方法中,我们可以直接使用$n这样的变量:
// 在String#replace()中引用匹配到的捕获组
var re = /(are)\s(you)/;
var source = 'are you ok';
var result = source.replace(re, '$2 $1');
console.log(result);
// output:
// "you are ok"可以看到,匹配到的are和you调换了位置,其中$1表示are,$2表示you。
在正则中,我们还可以使用\1 ... \n这样的子表达式,来表示前面匹配到的捕获组,我们称为反向引用。\1会引用前面第一个匹配到的捕获组,\2会引用第二个,依次类推。下面这个例子,我们用来匹配一对p标签的内容:
var re = /<(p)>.+<\/\1>/;
re.exec('<div><p>hello</p></div>'); // ["<p>hello</p>", "p"]从结果集中可以看到,我们成功匹配到了p标签的全部内容,而结果集中索引为1的元素,正是(p)捕获组匹配的内容。
非捕获组
非捕获组也可以理解为非记忆性捕获组,匹配内容但不记住匹配的结果,也就是说,匹配到的内容不会出现在结果集中。
我们用(?:x)的形式表示非捕获组,下面例子演示了捕获组和非捕获组的不同之处:
// 普通捕获组
var re = /he(llo)/;
re.exec('hello'); // ["hello", "llo"]
// 使用(?:llo)时 "llo"只匹配但不会出现在结果集中
var re = /he(?:llo)/;
re.exec('hello'); // ["hello"]
惰性模式
我们上面元字符部分提到的几个表达式,例如:
x* x+ x? x{n} x{n,} x{n,m}
他们默认情况是贪婪模式,就是尽可能的匹配更多的内容。比如下面的例子中,我们想匹配第一个HTML标签,由于默认是贪婪模式,它会匹配整个字符串:
var re = /<.*>/;
re.exec('<p>hello</p>'); // ["<p>hello</p>"]这并不是我们想要的结果,该怎么办呢?
我们需要在这些表达式后面追加一个问号,表示惰性模式,让正则匹配尽可能少的内容,上面的几个表达式将会变为下面这样:
x*? x+? x?? x{n}? x{n,}? x{n,m}?
稍微改一下上面的例子,我们来看看结果如何:
var re = /<.*?>/;
re.exec('<p>hello</p>'); // ["<p>"]
断言
所谓断言,是在指定子表达式的前面或后面,将会出现某种规则的匹配,只有匹配了这个规则,子表达式才会被匹配成功。断言本身并不会被匹配到结果数组中。
JavaScript语言支持两种断言:
零宽度正预测先行断言,表示为 x(?=y) ,它断言x后面会紧跟着y,只有这样才会匹配x。
零宽度负预测先行断言,表示为 x(?!y) ,它断言x后面不是y,只有符合此条件才会匹配x。
下面例子演示了这两个条件相反的断言:
// hello后面是world时 才匹配hello
var re = /hello(?=world)/;
re.exec('helloworld'); // ["hello"]
re.exec('hellojavascript'); // null
// 与上面结果相反 hello后面不是world是 才匹配hello
var re = /hello(?!world)/;
re.exec('helloworld'); // null
re.exec('hellojavascript'); // ["hello"]在断言的部分,我们还可以使用更具表达力的条件:
var re = /hello(?=world|javascript)/;
re.exec('helloworld'); // ["hello"]
re.exec('hellojavascript'); // ["hello"]
var re = /hello(?=\d{3,})/;
re.exec('hello33world'); // null
re.exec('hello333world'); // ["hello"]以上就是断言部分。关于正则表达式的内容也就先到这里了,前后一共三篇文章,涵盖了JavaScript正则的大部分内容,希望对同学们会有帮助。