正则表达式之捕获

最近刚好搞定CSS选择器命名的问题(勉强能够心平气和的写样式表,姑且算作是解决了一个疑问吧),然后开始阅读jQuery源码,虽然很早之前就曾经打开看过,当时看见弟六十几行定义的几个正则,然后一脸蒙蔽,悻悻然关闭了编辑器。然而,jQuery源码迟早是要读的,正则也是必须要掌握的,最重要的一点是:并没有什么知识是不可能学会的!既然如此,那就先从正则入手吧。

<!--more-->

1. exec()方法

jQuery前面定义的这几个正则用来进行快速匹配和单标签匹配:

rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,
rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/,

可以看见中间的(?:)。那么,这个带括号的问号究竟是什么呢?经过一番折腾,终于对正则表达式的"捕获"有了一点认识。这是参考的一篇文章。 在前面的《初识正则表达式》这篇文章中,对于小括号的作用只是进行了简单的描述:改变限定符的作用范围和分组

// 改变作用域范围
var re1 = /(a|b)c/;
// 分组
var re2 = /(ab){2}c/;

这里的分组,实际上是创建子表达式,提到子表达式,就不得不再提到一个更重要的正则对象方法:exec()。exec()方法用来返回字符串中符合对应正则表达式的子字符串。

var re = /\d{2}/;
console.log(re.exec("asd78aa"));//["78", index: 3, input: "asd78ss"]
console.log(re.exec("asdaa));//null

可以看见,如果匹配成功,实际上exec返回的是一个数组arr,arr[0]表示所匹配到的文本,index表示该文本的索引值,input即为原字符串。但是,这个返回值并不仅仅有这些属性,如果正则表达式中存在子表达式的时候:

var re = /(\d{2})aa/;
console.log(re.exec("asd78aa"));//["78aa", "78", index: 3, input: "asd78aa"]

则对应的arr[1]为正则表达式中第一个子表达式所匹配到的值,如果存在多个子表达式(也就是多个分组括号),则从左到右依次将子表达式所匹配到的文本存放在对应索引值位置

//从arr[1]开始,依次存放子表达式匹配值
var re1 = /(\d{2})(aa)/;
console.log(re1.exec("asd78aa"));//["78aa", "78", "aa", index: 3, input: "asd78aa"]

// 嵌套情况下也是从左到右
var re2 = /(\d{2}(xyz))aa/;
console.log(re2.exec("asd78xyzaa"));//["78xyzaa", "78xyz", "xyz", index: 3, input: "asd78xyzaa"]

// undefined
var re3 = /(\d{2})|(xyz)/;
console.log(re3.exec("asd78xyz"));//["78", "78", undefined, index: 3, input: "asd78aaxyz"]

上面的undefined是什么鬼?这是因此没有设置全局模式的缘故,第一次返回匹配结果(78)的时候,并没有匹配到第二个子字符串(xyz)的文本,因此使用undefined进行填充(注意,这里是理解捕获与非捕获十分重要的一点,也就是说,实际上无论匹配是否成功,默认都会对对应索引值元素进行填充)。

//加上全局模式
var re3 = /(\d{2})|(xyz)/g;
console.log(re3.exec("asd78xyz"));//["78", "78", undefined, index: 3, input: "asd78xyz"]
console.log(re3.exec("asd78xyz"));//["xyz", undefined, "xyz", index: 5, input: "asd78xyz"]
console.log(re3.exec("asd78xyz"));//null
//此时lastindex重置为0
console.log(re3.exec("asd78xyz"));//["78", "78", undefined, index: 3, input: "asd78xyz"]

也就是说,可以通过判断返回结果是否为null,可以获取整个字符串中完整模式匹配信息。

2. 捕获

2.1. 定义

使用小括号指定一个子表达式后,匹配这个子表达式的文本(即匹配的内容,也就是上面的arr[1],arr[2]...的值)可以在表达式或者其他过程中进一步进行处理,这个大概就是捕获的定义。 所谓的进一步处理,实际上是使用子表达式的结果来限定实际正则表达式(整个正则表达式)的输出结果(限定这个词是我自己理解所想到的词儿,若有错误还请指正)。这些限定包括:非捕获组(?:),前查找(?=),后查找(?<=)。

2.2. 非捕获组

在某些情况下:比如需要使用某个限制条件匹配正则,但是又不希望在输出结果中看键这个限制条件。这时,就可以使用非捕获分组来达到这种效果。

var re = /(?:\d{2})xyz/g;
console.log(re.exec("asd78xyz"));//["78xyz", index: 3, input: "asd78xyz"]

可以看见,虽然存在子表达式(\d{2}),但是并没有在exec()输出结果中看见该子表达式对应的匹配(前面提到,即使没有匹配到结果,也会使用undefined进行占位)。使用非捕获组的用途,大概就是为了去除输出结果中无效的undefined的吧(有可能不是这么回事儿)。

2.3. 前查找

仍然考虑一种情况:我们需要匹配的目标字符串,是通过其后面的文本特征进行匹配的(比如找到后三位是xyz的目标文本)。这种情况下就可以使用前查找(?=)。

var re = /\d{2}(?=xyz)/g;
console.log(re.exec("asd78xyz"));//["78", index: 3, input: "asd78xyz"]

如果是连续的字符串匹配会发生什么情况呢?

var re = /xyz(?=xyz)/g,result;
while ((result= re.exec("asd78xyzxyzxyzxyz")) != null) {
    console.log(result);
    //["xyz", index: 5, input: "asd78xyzxyzxyzxyz"]
    //["xyz", index: 8, input: "asd78xyzxyzxyzxyz"]
    //["xyz", index: 11, input: "asd78xyzxyzxyzxyz"]
}
```javascript
通过查看index属性可以看到,每次匹配结束后的下一次匹配,是从刚才匹配过的(xyz)结尾的字符开始的(有点绕,实际上第一次匹配,只是确认目标字符串存在指定的那个限定条件,并不影响该限定条件位于的文本参加下一次匹配)。
实际上前查找还有一种写法(?=\d{2}xyz)\d{2}
```javascript
var re = /(?=\d{2}xyz)\d{2}/;
// 实际上跟/\d{2}(?=xyz)/是等效的
console.log(re.exec("asd78xyz"));//["78", index: 3, input: "asd78xyz"]

看上去这种写法并没有前面那种表达式直白,毕竟前面的写法更能表达“目标表达式后面存在XXX限定条件”的意思。 再来一点有意思的:如果要匹配“目标表达式后面不存在XXX限定条件”这样的文本该怎么做呢?只要把"="换成"!"即可,也就是(?!):

var re = /\d{2}(?!xyz)/;
console.log(re.exec("asd78aa"));//["78", index: 3, input: "asd78aa"]

只是跟前面的限定条件完全相反,其他是一样的。

2.4. 后查找

故名思意,这个捕获需要匹配“其前面存在XXX限定条件的目标表达式”,然而!!!JS并不支持后查找(?<=),呃,这特么就尴尬了。也许将怎么字符串反转,再将目标表达式反转,然后再使用查找....-_-||好麻烦的样子。 上论坛然后有之前的讨论里面提到了这篇帖子,用以参考。 那么如何实现后查找呢?(待我挖个坑,后面再回来填)。