JavaScript:正则表达式的/y标识
本文要讲的是一个ES6特性——正则表达式对象的/y标志的作用.该特性同时也是一个ES4特性,所以Firefox3+都原生支持,其他浏览器目前还没有实现.
和/i对应于re.ingoreCase类似,/y标识对应的属性是re.sticky(实际上这个y来自于yylex),sticky的意思是"粘滞".
和"全局匹配"类似,sticky属性为true的正则表达式对象(有/y标识)的匹配过程称之为"粘滞匹配"(反正我就这么叫了).
粘滞匹配会从两个方面影响正则表达式的匹配方式.如下:
1.读取和设置lastIndex属性的值
以防你不知道lastIndex的作用,首先给一个最简单的例子:
var re = /o/;
print(re.lastIndex); //0,lastIndex属性的初始值为0
print(re.test("foo")); //true print(re.test("foo")); //true print(re.test("foo")); //true print(re.lastIndex); //0,lastIndex属性的值没有被更新
re.lastIndex = 10; //手动修改lastIndex属性的值
print(re.test("foo")); //true,同样可以匹配
print(re.lastIndex); //10
这应该是最常见的情况,如果一个正则对象不是全局(global)的,则其lastIndex属性会被完全忽略(不会读取,也不会赋值).但如果:
var re = /o/g; //全局匹配
print(re.lastIndex); //0,lastIndex属性的初始值为0 print(re.test("foo")); //true,匹配了第二个字符 print(re.lastIndex); //2,lastIndex属性的值被设置为2,也就是第二个字符之后 print(re.test("foo")); //true,从第二个字符(lastIndex属性的值)之后开始匹配,匹配了第三个字符 print(re.lastIndex); //3,lastIndex属性的值被设置为2,也就是字符串的末尾 print(re.test("foo")); //false,已经没有字符了,匹配失败. print(re.lastIndex); //0,lastIndex属性的值被重置为0 print(re.test("foo")); //true,又一次重新开始匹配 print(re.lastIndex); //2,一直循环下去 re.lastIndex = 3; //手动修改为3 print(re.test("foo")); //false,从第三个字符后开始匹配,所以匹配失败
从上例中可以看出,全局匹配(/g)操作会读取还会更新lastIndex属性的值.类似于全局匹配,粘滞匹配(/y)也会让这样做.不过会有点区别,就是如果是全局匹配,在匹配失败后,lastIndex属性的值会被重置为0,但粘滞匹配不会.看下面的例子:
var re = /./y; //粘滞匹配
print(re.test("foo")); //true print(re.lastIndex); //1 print(re.test("foo")); //true print(re.lastIndex); //2 print(re.test("foo")); //true print(re.lastIndex); //3 print(re.test("foo")); //false print(re.lastIndex); //3,匹配失败后,lastIndex属性没有归零 print(re.test("foo")); //false,所以匹配会一直失败下去
re.lastIndex = 0; //同样可以手动修改lastIndex属性的值
print(re.test("foo")); //true
只有全局匹配和粘滞匹配才会让引擎在匹配过程执行完毕后更新正则对象的lastIndex属性的值,我们可以从SpiderMonkey源码(jsregexp.cpp)中清晰的看到这一过程:
/* Update lastIndex. */ if (re->global() || (!rval->isNull() && re->sticky())) { //如果是全局匹配或者(粘滞匹配且匹配成功时),才会进入下面的语句块 if (rval->isNull()) //如果匹配失败,也就是全局匹配的匹配失败 obj->zeroRegExpLastIndex(); //则把lastIndex属性的值归零 else //否则,也就是全局匹配或者粘滞匹配匹配成功的情况 obj->setRegExpLastIndex(lastIndexInt); //则把lastIndexInt属性的值设置为成功匹配的子字符串在原字符串中的偏移量 }
2."粘滞"的真正意义
看了第一节的介绍,你会发现我只提到了全局匹配和粘滞匹配的一个区别,就是在匹配失败后要不要将lastIndex属性的值重置为0.只有这些吗?当然不是.这根本没有体现出"粘滞"到底是什么意思.
我只用一个超简单的例子就能演示出"粘滞"到底表现为什么:
/o/.test("foo") //true /o/y.test("foo") //false
这个表现你能想通吗?我用文字表述一下就是:粘滞匹配不能像普通正则那样,跳过那些匹配失败的字符后接着执行后面字符的匹配操作.在本例中就是,/o/在匹配字符f失败之后,就完全失败了,不会接着匹配f后面的o.为什么会这样?我们还得和全局匹配比较一下:
var re = /^./g; print(re.test("foo")); //true print(re.lastIndex); //1 print(re.test("foo")); //false print(re.lastIndex); //0
这个例子应该好懂,因为有^锚点(匹配字符串的开头位置),所以第二次的匹配会失败.然而:
var re = /^./y; print(re.test("foo")); //true print(re.lastIndex); //1 print(re.test("foo")); //true print(re.lastIndex); //2
结论就是:全局匹配和粘滞匹配都会从字符串中由正则对象的lastIndex属性的值指定的偏移位置处开始匹配,但区别是:粘滞匹配中,^元字符的意义变了,它代表的不是整个字符串的开头位置,而代表的就是这个偏移位置.所以上面的两次匹配都能成功.同时,每个粘滞正则中不管第一个字符是不是^元字符,都会被隐式的加上^.回到刚才那个让然费解的例子就是:
/o/y.test("foo")
就相当于
/^o/y.test("foo")
这下能看懂了吧.^f才能匹配,^o不行.
为了再次证明一下粘滞匹配中的^的位置会动态改变,看下面的例子:
var re = /o/y; //相当于/^o/y print(re.test("foo")); //false print(re.lastIndex); //0 re.lastIndex = 1; //手动跳过了第一个字符f,^现在匹配的位置就是f和o之间的位置,所以^o能匹配. print(re.test("foo")); //true print(re.lastIndex); //2,^现在匹配的位置就是o和o之间的位置,所以^o能匹配. print(re.test("foo")); //true print(re.lastIndex); //3 print(re.test("foo")); //false print(re.lastIndex); //3
现在懂了吧,粘滞的意思就是"使用隐式的^锚点把正则锚定在了lastIndex所指定的偏移位置处".
总结
网上几乎没有讲正则的/y标识的帖子或文章,Brendan Eich说的一段话给了我很大帮助来理解这个东西.
/y标识让一个未锚定的正则只在目标字符串的当前位置匹配成功或匹配失败./g或其他东西也会影响当前位置具体在哪里.但比起其他因素,/y是完全独立的(更底层).
本文中我只使用了正则的test方法来讲解/y标识,你应该再用其他正则相关的方法试验一下:RegExp.prototype.exec,String.prototype.search,String.prototype.split,String.prototype.match,String.prototype.replace.以及gy标识同时存在的情况等.
另外,其他语言比如Perl(正则表达式最强大最灵活的语言)中,怎么没有这个标识呢?答案是:人家有\G元字符,\G是个零宽断言,表示的是上次成功匹配时的偏移位置.
my $str = "foo"; pos($str) = 0; #相当于设置lastIndex为0 while ($str =~ /(\Go)/g) { #匹配失败,不会进入这个循环 print $1; } pos($str) = 1; #相当于设置lastIndex为1,跳过字符f while($str =~ /(\Go)/g) { #匹配成功,进入循环 print $1; #输出两次o print pos($str) #第一次输出2,第二次输出3 }
还有,比起其他语言中的正则,JavaScript中的正则其实是"弱爆了",如果这都学不会,呵呵...
对我来说,没有正则几乎相当于没有Google.