ES6 正则表达式

在讲ES6的正则表达式新特性之前首先复习一下正则表达式

  • 字符类表示
字符匹配用途
.除换行符和其他Unicode行终止符之外的任意字符适合匹配一行文本的任意字符
\w任何ASCII字符组成的单词,等价于[a-zA-Z0-9_]匹配字
\W任何非ASCII字符组成的单词,等价于[^a-zA-Z0-9_]匹配非字
\s任意Unicode空白符匹配空白符,[\s\S]可表示所有字符
\S任意非Unicode空白符的字符同上
\d任意ASCII数字,等价于[0-9]匹配数字
\D任意非ASCII数字,等价于[^0-9]匹配非数字
\cXX为A-Z中的一个,\cX为ctrl+X
\n换行符
\r回车符
\t水平制表符
\v垂直制表符
[...]匹配方括号内的任意字符
[^...]匹配不包括括号内的任意字符
  • 重复
字符含义
{n,m}匹配前一项至少n次,但不能超过m次
{n,}匹配前一项至少n次
{n}匹配前一项n次
?匹配前一项0或1次
+匹配前一项至少1次以上
*匹配前一项0或多次
  • 非贪婪重复

在重复标识符后加?就表示非贪婪重复,尽可能少的匹配,不过无法匹配时还是按照贪婪匹配,比如在全局模式下,非贪婪重复就没有作用了

console.log("--------非贪婪重复----------");
var z = "<div>a</div><div>b</div>";
console.log(z.match(/<div>.*?<\/div>/));
console.log(z.match(/<div>.*<\/div>/)); 

结果如下:

--------非贪婪重复----------
[ '<div>a</div>', index: 0, input: '<div>a</div><div>b</div>' ]
[ '<div>a</div><div>b</div>',
 index: 0,
 input: '<div>a</div><div>b</div>' ] 
  • 分组和引用
console.log("----------捕获分组----------");
var a = '\'asdfsdf\'';
console.log(/(['"])[^'"]*\1/ig.test(a));
var b = '1+2';
var c = /\d(?!\+)/;
var d = /\d(?=\+)/;
console.log(c.exec(b));
console.log(b.match(c));
console.log(b.match(d)); 

结果如下:

----------捕获分组----------
true
[ '2', index: 2, input: '1+2' ]
[ '2', index: 2, input: '1+2' ]
[ '1', index: 0, input: '1+2' ] 

()的作用很多,除了将单独的项组合成子表达式,还可以允许同一表达式后部引用前面的子表达式,如上面的代码,我们就对'进行了引用,使得''""成对匹配,其中()分组从\1开始引用,接着是\2...

(?!exp)和(?=exp)

还有零宽匹配,这赋予了正则一定的逻辑判断能力,上面的代码\d(?!\+)就是匹配后面不是+号的数字,即2;\d(?=\+)就是匹配后面是+号的数字,即1

\b和\B

和零宽匹配相关的还有两个\b\B,主要用于单词截取

console.log("----------\\b\\B----------");
var e = 'éexoé';
console.log(e.match(/\be\b/));
console.log(e.match(/\bexo\b/));
var f = 'possibly yesterday';
var g = 'possibly yésterday';
var h = /y\B/; // ->/y(?=[\w])/
var i = /y\b/; // ->/y(?=[\W])/
var j = /y(?=[\W])/;
console.log(f.match(h));
console.log(f.match(i));
console.log(g.match(h));
console.log(g.match(i));
console.log(g.match(j)); 

结果如下:

----------\b\B----------
null
[ 'exo', index: 1, input: 'éexoé' ]
[ 'y', index: 9, input: 'possibly yesterday' ]
[ 'y', index: 7, input: 'possibly yesterday' ]
null
[ 'y', index: 7, input: 'possibly yésterday' ]
[ 'y', index: 7, input: 'possibly yésterday' ] 

\b其实和(?=[\w])对应

\B其实和(?=[\W])对应

  • JS中使用正则的各个方法
  1. String.prototype.match
console.log("----------match()/exec()----------");
var o = /(java)/g;
var text = 'javascript is more fun than java';
var result;
//while((result = o.exec(text))!=null) { 
//  console.log("Matched " + result[0] + " Position " + result.index + " Next Begins " + o.lastIndex);
//}

console.log(o.exec(text));
console.log(o.lastIndex);
console.log(o.exec(text));
console.log(o.lastIndex);
console.log(text.match(o));
console.log(o.lastIndex); 
----------match()/exec()----------
[ 'java',
 'java',
 index: 0,
 input: 'javascript is more fun than java' ]
4
[ 'java',
 'java',
 index: 28,
 input: 'javascript is more fun than java' ]
32
[ 'java', 'java' ]
0 

可以看到在全局搜索模式下,exec执行后也只是执行一次匹配,但是RegExp对象中lastIndex指向第二次开始匹配的开始位置,所以这里要注意了,如果重新开始匹配新的字符串,记得收到将lastIndex置为0,而match命令则进行了全局搜索返回了一个数组lastIndex对match方法似乎也毫不影响

现在看下,正则表达式在非全局模式下的表现

----------match()/exec()----------
[ 'java',
 'java',
 index: 0,
 input: 'javascript is more fun than java' ]
0
0
[ 'java',
 'java',
 index: 0,
 input: 'javascript is more fun than java' ]
0 

我们发现,match()和exec()的做法似乎完全一样

另外还有两者返回的数组属性,其中第二个java指的是分组数据,因为我们使用了分组,所以$1会被装载到arr[1]中,index指的是开始匹配的位置,input是原始数据

  1. String.prototype.split
console.log("---------split()----------");
var k = '1,2;3 . 5>6';
var l = /\s*[,;>\.]\s*/;
console.log(k.split(l));
console.log(k.split(l, 2)); 

结果如下:

---------split()----------
[ '1', '2', '3', '5', '6' ]
[ '1', '2' ] 

split的第二个参数用以限制分割的数组

  1. String.prototype.search
var k = '1,2;3 . 5>6';
console.log(k.search(/\d>\d/)); 

返回了是匹配到的字符的开始索引

  1. String.prototype.replace
console.log("----------powerful replace()----------");
// 第二个参数可以是函数
function styleHyphenFormat(propertyName) {
 function upperToHyphenLower(match) {
   return "-" + match.toLowerCase();
 }
 return propertyName.replace(/[A-Z]/g, upperToHyphenLower);
}

console.log(styleHyphenFormat("borderTop"));

// 还有替换参数$&, $`,$'
var m = 'a-b';
console.log(m.replace(/(\w+)-(\w+)/g, '$2-$1'));
console.log(m.replace(/-/, '$&-'));
console.log(m.replace(/-/, '$`$`'));
console.log(m.replace(/-/, '$\'$\''));


// 还有每匹配到一个模式都可以执行函数,且可以将分组结果传到参数当中
var n = "101010101010010101010110";
var arr1 = [];
var arr2 = [];
n.replace(/(10)|(01)/g, function(match, p1, p2){
 if(p1) arr1.push(match);
 if(p2) arr2.push(match);
});
console.log(arr1);
console.log(arr2); 

结果如下:

----------powerful replace()----------
border-top
b-a
a--b
aaab
abbb
[ '10', '10', '10', '10', '10', '10', '10' ]
[ '01', '01', '01', '01', '01' ] 
  1. RegExp.prototype.exec
    见1
  2. RegExp.prototype.test
console.log(/.*(java).*/.test('javafffffff')); 
  • 修饰符

m修饰符,多行匹配,改变了$和^的行为模式

console.log("----------flags:m----------");
var x = "abababababc\nababababab";
console.log(x.match(/c$/));
console.log(x.match(/c$/m)); 

结果如下:

----------flags:m----------
null
[ 'c', index: 10, input: 'abababababc\nababababab' ] 

ES6添加的特性

  • 修饰符u

使得js正则能够正确匹配双字节字符

console.log("----------flags:u----------");
var reg = new RegExp("^\u{20BB7}", "u");
console.log(reg.test("𠮷"));
console.log(reg.flags); 

结果如下:

----------flags:u----------
true
u 

如果没有u

console.log(/\u{20BB7}/.test("𠮷")); //false 

不过这样的结果确是true

var reg = new RegExp("\u{20BB7}");
console.log(reg.test("𠮷")); // true 

我猜测RegExp对象对于\u{}这样形式的匹配模式,会将它看做加了u

  • 修饰符y

y是sticky修饰符,粘连修饰符,其设计的含义就是让头部修饰符^在全局匹配中均有效,y和g一样是全局匹配,不过在match方法中需要加上g才能全局匹配,如下:

console.log("----------flags:y----------");
// 这里我们可以一窥split和sticky修饰符的的工作原理
// 每匹配到一个#作为分隔符,分隔符前面的字符作为一个分量
// 很显然,我们必须将分割符放在字符串头部才能进行split分割
// 所以#x#x#x得到的第一个分量是空字符串,而后继续匹配x#x#x,匹配到#,当前字符串并不是以#开头的,不予分割一直到最后也没能匹配到一个所以第二个分组为x#x#x
// 而##x则显而易见,分量为空字符串,空字符串,x
console.log('#x#x#x'.split(/#/y));
console.log('##x'.split(/#/y));
console.log('a1a2a3'.match(/a\d/y));
console.log('a1a2a3'.match(/a\d/gy));

// 一个sticky修饰符的应用
const TOKEN_Y = /\s*(\+|(0|[1-9]\d*))/y;
const TOKEN_G = /\s*(\+|(0|[1-9]\d*))/g;
function tokenize(TOKEN_REGEX, str) { 
 let result = [];
 let match;
 while((match = TOKEN_REGEX.exec(str))!=null) { 
   result.push(match[1]);
 }
 return result;
}

console.log(tokenize(TOKEN_Y, '12 + 4'));
// 只能匹配到12,x阻碍了后续的匹配
console.log(tokenize(TOKEN_Y, '12x + 4'));
console.log(tokenize(TOKEN_G, '12 + 4'));
console.log(tokenize(TOKEN_G, '12x + 4')); 

结果如下:

----------flags:y----------
[ '', 'x#x#x' ]
[ '', '', 'x' ]
[ 'a1', index: 0, input: 'a1a2a3' ]
[ 'a1', 'a2', 'a3' ]
[ '12', '+', '4' ]
[ '12' ]
[ '12', '+', '4' ]
[ '12', '+', '4' ] 

一些提案

  • RegExp.escape()

字符串必须经过转义,把字符串中正则的特殊字符进行转义才能作为正则表达式

原先我们可以这样实现

function escapeRegExp(str) {
 return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
}

let str = '/path/to/resource.html?search=query';
escapeRegExp(str)
// "\/path\/to\/resource\.html\?search=query" 

现在有个提案,引入静态方法Regex.escape()将字符串,转化为正则表达式

虽然是提案,不过有垫片库

regex.ecape

  • 修饰符s,doAll模式

原先符号.匹配非行终止符的任意字符,但是使用s修饰符可以让.匹配包括行终止符在内的所有字符

  • 后行断言

之前我们学习了(?=exp)(?!exp),他们称为前行断言,只能匹配在他们之前的字符也就是说,正则是先找到这个字符,然后判断他们后面的字符是否匹配或者不匹配exp,如果是,匹配成功;而后行断言正好相反(?<=exp)(?<!exp),正则先找到该模式后字符匹配,然后判断字符前是否和exp相匹配或不相匹配,如果是,匹配成功

var b = '1+2';
var sd = /(?=\+)\d/;
console.log(b.match(sd)); // null 
var b = '1+2';
var sd = /(?<=\+)\d/;
console.log(b.match(sd)); // 2 

这个在chrome中输入chrome://flags,然后开启其中的实验性javascript可以做试验

参考文献


write by jeffwang 2017/03/11