在正则表达式中,括号涉及的问题比较多,所以这里单独拿出来讲。
分组
如果量词所限定的元素不是一个字符或者字符组,而是一系列字符或者子表达式,就需要使用括号将他们括起来,表示为“一组”,构成单个元素。
var regex = /(ab)+/g;
var string = "ababa abbb ababab";
console.log( string.match(regex) );
// ["abab", "ab", "ababab"]
上面的例子中,量词 + 的前面的元素是 (ab) , 所以 + 所限定的是括号内 ab 这个整体。
划定多选结构的范围
多选结构, 也叫 分支结构。一般的用法: (p1|p2|p3),其中,| 表示 “或”,p1、p2 和 p3 是三个子表达式,这些子表达式也叫多选分支, 括号用来划定分支结构的范围。
注意:多选结构中括号不是必须的。如果没有括号,管道符 | 会把整个表达式当做一个多选结构。比如,要匹配 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,那么二者什么区别呢?
二者差别还是很大的:
多选结构中每个分支都必须明确列出。而字符组可以使用 - 表示范围
大多数情况下, [abc] 要比 (a|b|c) 更高效
字符组的每个 “分支” 都必须是单个的字符,而多选结构的“分支”可以是子表达式
多选结构的分支顺序会影响到最后的配置结果
没有 排除型多选结构
引用分组
使用括号之后,正则表示会保存每个分组真正匹配的文本,等匹配成功后,可以引用这些文本。
因为这种情况下“捕获”了文本,所以这种分组叫 捕获分组,这种括号叫 捕获型括号。
通过编号引用
编号规则:
如,使用(\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
replace方法-替换
比如,想把 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-30、2018.12.30 和 2018/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 的处理是:
- 如果存在第 10 个捕获分组,则引用对应的分组
- 如果不存在,则引用 \1
如果,在有第 10 个捕获分组的情况下,要匹配 \1 和 字符0 的话,可以使用下面两种方法:
- 命名分组 再使用
- 括号将 \1 或 0 括起来,比如 (\1)0 或 \1(?:0)
命名分组
由于按编号引用分组存在一些问题,如:可读性差,不易维护,二义性等。于是出现了命名分组,使用易记忆,易辨别的名字来代替编号。
== 注意:命名分组是 ES2017 新特性。==
语法规则如下:
分组:(?)
提取:$
反向引用:\k
比如,上文的一个例子可以改为:
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)。这种只用来限定范围不捕获匹配文本的分组就是 非捕获分组。
非捕获型分组的优点是性能好,缺点是不美观,可读性差。
在实际应用中,建议尽量使用非捕获分组。
原文链接:https://www.jianshu.com/p/6a7d5450e164