JavaScript从入门到放弃

JavaScript 与正则表达式 -- 括号

2018-12-31  本文已影响0人  菜鸟很浮夸

在正则表达式中,括号涉及的问题比较多,所以这里单独拿出来讲。

分组

如果量词所限定的元素不是一个字符或者字符组,而是一系列字符或者子表达式,就需要使用括号将他们括起来,表示为“一组”,构成单个元素

var regex = /(ab)+/g;
var string = "ababa abbb ababab";
console.log( string.match(regex) );  
 //  ["abab", "ab", "ababab"]

上面的例子中,量词 + 的前面的元素是 (ab) , 所以 + 所限定的是括号内 ab 这个整体。

划定多选结构的范围

多选结构, 也叫 分支结构。一般的用法: (p1|p2|p3),其中,| 表示 “或”,p1p2p3 是三个子表达式,这些子表达式也叫多选分支, 括号用来划定分支结构的范围。
注意:多选结构中括号不是必须的。如果没有括号,管道符 | 会把整个表达式当做一个多选结构。比如,要匹配 grey或gray:

var regexRight = /gr(e|a)y/;  // 匹配 grey 或 gray
var regexWrong = /gre|ay/;  // 匹配 gre 或 ay

// 正确的
console.log(regexRight.test('grey'));  // true
console.log(regexRight.test('gray'));  // true
console.log(regexRight.test('gre'));  // false

// 错误的
console.log(regexWrong.test('grey'));  // true
console.log(regexWrong.test('gre'));   // true

所以,虽然多选结构中括号不是必须的,但是,通常会搭配括号来使用。

多选结构与字符组

上面多选结构中 gr(e|a)y的例子并太好,因为可以使用更好的方式代替,那便是 gr[ae]y,那么二者什么区别呢?
二者差别还是很大的:

引用分组

使用括号之后,正则表示会保存每个分组真正匹配的文本,等匹配成功后,可以引用这些文本。
因为这种情况下“捕获”了文本,所以这种分组叫 捕获分组,这种括号叫 捕获型括号

通过编号引用

编号规则:
如,使用(\d{4})-(\d{2})-(\d{2})匹配日期 2018-12-30:

字符串 2018 12 30
表达式 (\d{4}) (\d{2}) (\d{2})
分组编号 1 2 3

注意:
如果把表达式写成:(\d){4}-(\d){2}-(\d){2},则含义完全不同,(\d){4} 表示 \d 作为单独的元素出现4次,且编号都为1。

嵌套规则:根据开括号的出现顺序来计数。(图参考《正则指引》P45,我画的有点丑)

括号嵌套编号规则:开括号的出现顺序

在 JavaScript 中使用

提取数据

String.prototype.match() 方法返回一个数组,数组的第一项是进行匹配的完整字符串,之后的项是捕获分组的匹配结果。

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var text = '2018-12-30';
console.log(text.match(regex));
// ["2018-12-30", "2018", "12", "30", index: 0, input: "2018-12-30"]

关于 match 方法,有一个地方需要注意,返回结果与正则表达式是否包含 g 标志有关。在没有 g 标志的时候,返回值和 regex.exec() 方法相同:

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var text = '2018-12-30';
console.log(regex.exec(text));
// ["2018-12-30", "2018", "12", "30", index: 0, input: "2018-12-30"]

同时,也可以使用构造函数的全局属性 $1$9 来获取引用:

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var text = '2018-12-30';
regex.exec(text);

console.log(RegExp.$1);  // 2018
console.log(RegExp.$2);  // 12
console.log(RegExp.$3);  // 30

替换

比如,想把 yyyy-mm-dd 格式,替换成 mm/dd/yyyy 怎么做?
可以使用下面的三种方法:

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var text = '2018-12-30';

// 1
var result1 = text.replace(regex, '$2/$3/$1');

// 2
var result2 = text.replace(regex, () => `${RegExp.$2}/${RegExp.$3}/${RegExp.$1}`);

// 3
var result3 = text.replace(regex, (str, y, m, d) => `${m}/${d}/${y}`);

console.log(result1);    // 12/30/2018
console.log(result2);    // 12/30/2018
console.log(result3);    // 12/30/2018

String.prototype.replace() 规则相对复杂,有很多玩法,了解更多

反向引用

在正则表达式内部引用之前(左侧)捕获分组匹配的文本,形式如:\num ,其中 num 表示编号,编号规则与之前介绍的相同。
举个例子:
比如要匹配: 2018-12-302018.12.302018/12/30 三种形式。
可能首先想到的是:\d{4}(-|\/|\.)\d{2}(-|\/|\.)\d{2},但是:

var regex = /\d{4}(-|\/|\.)\d{2}(-|\/|\.)\d{2}/;
var text = '2018-12.30';
console.log(regex.test(text));  // true

显然,我们不希望匹配 2018-12.30 ,我们需要前后的分隔符相同:

var regex = /\d{4}(-|\/|\.)\d{2}\1\d{2}/;
var text1 = '2018-12.30';
var text2 = '2018-12-30';
var text3 = '2018/12/30';

console.log(regex.test(text1));  // false
console.log(regex.test(text2));  // true
console.log(regex.test(text3));  // true

这里的 \1 就是对前面 (-|\/|\.) 的引用,表达式可视化如下:

反向引用可视化

反向引用的二义性:

在反向引用中,如果编号大于9就会出现二义性,如:\10 是表示第十个捕获分组呢还是表示第一个捕获分组和一个字符 0 呢?
在一些编程语言中有专门的规定来避免二义性,但是在JavaScript中并没有,JavaScript对于 \10 的处理是:

  1. 如果存在第 10 个捕获分组,则引用对应的分组
  2. 如果不存在,则引用 \1

如果,在有第 10 个捕获分组的情况下,要匹配 \1 和 字符0 的话,可以使用下面两种方法:

命名分组

由于按编号引用分组存在一些问题,如:可读性差,不易维护,二义性等。于是出现了命名分组,使用易记忆,易辨别的名字来代替编号。
注意:命名分组是 ES2017 新特性。

语法规则如下:

比如,上文的一个例子可以改为:

var regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
var text = '2018-12-30';
var result = text.replace(regex, '$<month>/$<day>/$<year>');

console.log(result);   // 12/30/2018

对于方法 String.prototype.match()RegExp.prototype.exec() 也有了新玩法:

var regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
var text = '2018-12-30';
var matchObj = text.match(regex);

console.log(matchObj.groups);
// {year: "2018", month: "12", day: "30"}

在匹配结果中,多了 groups 属性,保存了所有命名捕获分组的匹配结果。

再来看一个反向引用的例子:

var regex = /\d{4}(?<split>-|\/|\.)\d{2}\k<split>\d{2}/;
var text = '2018-12-30';

console.log(regex.test(text));  // true

非捕获分组

括号的功能有“叠加”性。括号可以表示分组,用来构成单个元素;也可以表示多选结构;但同时,也构成了引用分组。
在仅仅需要标记范围(分组或多选结构)时,正则表达式保存已经匹配的文本会造成不必要的性能浪费。
这时候我们可以使用 非捕获型括号 (?:...)来限定分组或多选结构的范围:(?:p)(?:p1|p2)。这种只用来限定范围不捕获匹配文本的分组就是 非捕获分组

非捕获型分组的优点是性能好,缺点是不美观,可读性差。
在实际应用中,建议尽量使用非捕获分组。

上一篇下一篇

猜你喜欢

热点阅读