JavaScript: 详解正则表达式之三

在上两篇文章中博主介绍了JavaScript中的正则经常使用方法正则修饰符,今天准备聊一聊元字符和高级匹配的相关内容。javascript

首先说说元字符,想必你们也都比较熟悉了,JS中的元字符有如下几种:html

/ \ | . * + ? ^ $ ( ) [ ] { }

它们都表示特殊的含义,下面咱们就来一一介绍它们。java

/ (slash)

用于建立一个字面量的正则表达式:es6

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),看下面例子:ide

/./.test('w');      // true
/./.test('$');      // true

/./.test('\r');     // false
/./.test('\n');     // false

但须要注意的是,若是遇到码点大于0xFFFF的Unicode字符,就不能识别了,必须加上u修饰符:ui

/^.$/.test('𠮷');   // false
/^.$/u.test('𠮷');  // true

* (asterisk)

用于匹配0到多个子表达式,也就是说,子表达式无关紧要,可多可少。若是咱们在单个字符后加上星号,它仅做为这个字符的量词,最终的匹配结果还与上下文有关,看下面例子:spa

/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次。咱们还用上面的例子,结果会有所不一样:code

/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正则的大部份内容,但愿对同窗们会有帮助。

本文完。

 

参考资料:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions

http://es6.ruanyifeng.com/#docs/regex

相关文章
相关标签/搜索