在上两篇文章中博主介绍了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正则的大部分内容,希望对同学们会有帮助。
本文完。
参考资料:
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