详解正则表达式

322 阅读17分钟

正则表达式是一个精巧的利器,经常用来在字符串中查找和替换,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/);       // -1

String#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正则的大部分内容,希望对同学们会有帮助。