括号的作用:括号提供了分组,便于我们引用它。
引用某个分组,会有两种情形:在 JavaScript 里引用它,在正则表达式里引用它。
内容包括:
分组和分支结构
分组引用
反向引用
非捕获括号
1、分组和分支结构
这二者是括号最直接的引用,也是最原始的功能,强调括号内的正则是一个整体,即提供子表达式。
1.1 分组
/a+/ 匹配连续出现的 "a",而要匹配连续出现的 "ab" 时,需要使用 /(ab)+/。
其中括号是提供分组功能,使量词 + 作用于 "ab" 这个整体,测试如下:
var regex = /(ab)+/g;
var string = "ababa abbb ababab";
console.log( string.match(regex) ); // ["abab", "ab", "ababab"]
1.2 分支结构
而在多选分支结构 (p1|p2) 中,此处括号的作用也是不言而喻的,提供了分支表达式的所有可能。
比如,要匹配如下的字符串,可以使用正则:
I love JavaScript
I love Regular Expression
var regex = /^I love (JavaScript|Regular Expression)$/;
console.log( regex.test("I love JavaScript") ); // true
console.log( regex.test("I love Regular Expression") ); // true
如果去掉正则中的括号,即:
/^I love JavaScript|Regular Expression$/,
匹配字符串是 "I love JavaScript" 和 "Regular Expression",当然这不是我们想要的。
2、分组引用
这是括号一个重要的作用,有了它,我们就可以进行数据提取,以及更强大的替换操作。
而要使用它带来的好处,必须配合使用实现环境的 API。
以日期为例。假设格式是 yyyy-mm-dd 的,我们可以先写一个简单的正则:
var regex1 = /\d{4}-\d{2}-\d{2}/;
var regex2 = /(\d{4})-(\d{2})-(\d{2})/;
console.log(regex1.test('2019-10-24')); // true
console.log(regex2.test('2019-10-24')); // true
对比这两个正则,我们发现,与前者相比,后者多了分组编号,如 Group #1。
其实正则引擎也是这么做的,在匹配过程中,给每一个分组都开辟一个空间,用来存储每一个分组匹配到的数据。
既然分组可以捕获数据,那么我们就可以使用它们。
2.1 提取数据
提取出年、月、日,可以这么做:
let regex = /(\d{4})-(\d{2})-(\d{2})/;
let date = "2019-11-09";
console.log(date.match(regex)); // ["2019-11-09", "9", "11", "09", index: 0, input: "2019-11-09", groups: undefined]
console.log(regex.exec(date)); // ["2019-11-09", "9", "11", "09", index: 0, input: "2019-11-09", groups: undefined]
NOTE
match 返回的一个数组,第一个元素是整体匹配结果,然后是各个分组(括号里)匹配的内容,然后是匹配下标,最后是输入的文本。另外,正则表达式是否有修饰符 g,match返回的数组格式是不一样的。
也可以使用构造函数的全局属性 $1
至 $9
来获取:
let regex = /(\d{4})-(\d{2})-(\d{2})/;
let date = "2019-11-09";
regex.test(date);
// regex.exec(date);
// date.match(regex);
console.log(RegExp.$1); // "2019"
console.log(RegExp.$2); // "11"
console.log(RegExp.$3); // "09"
2.2 替换
把 yyyy-mm-dd
格式替换成 mm/dd/yyyy
let regex = /(\d{4})-(\d{2})-(\d{2})/;
let date = "2019-11-09";
let result = date.replace(regex, "$2/$3/$1");
console.log(result); // 11/09/2019
其中replace
中的,第二个参数里用$1、$2、$3
指代相应的分组。等价于如下形式:
let regex = /(\d{4})-(\d{2})-(\d{2})/;
let date = "2019-11-09";
let result = date.replace(regex, () => {
return `${RegExp.$2}/${RegExp.$3}/${RegExp.$1}` ;
}) ;
console.log(result); // 11/09/2019
也等价于
let regex = /(\d{4})-(\d{2})-(\d{2})/;
let date = "2019-11-09";
let result = date.replace(regex, (match, year, month, day) => {
return `${month}/${day}/${year}` ;
}) ;
console.log(result); // 11/09/2019
3、反向引用
除了使用相应 API 来引用分组,也可以在正则本身里引用分组。但只能引用之前出现的分组,即反向引用。
写一个正则支持匹配如下三种格式:
2019-11-09
2019/11/09
2019.11.09
使用正则:
let regex1 = /\d{4}(-|\/|\.)\d{2}(-|\/|\.)\d{2}/;
let regex2 = /\d{4}(-|\/|\.)\d{2}\1\d{2}/;
regex1中
/
和.
需要转义。虽然匹配了要求的情况,但也匹配 2019-11/09 这样的数据。
regex2要求分割符前后一致,使用了反向引用。
注意里面的\1
,表示的引用之前的那个分组(-|\/|\.)
。不管它匹配到什么(比如-),\1
都匹配到那个同样的具体某个字符。
\2
和\3
分别指代第二个和第三个分组。
3.1 括号嵌套怎么办?
以左括号(开括号)为准,比如:
let regex = /^((\d)(\d(\d)))\1\2\3\4$/;
let test = "1231231233";
console.log(regex.test(test));
console.log(RegExp.$1); // 123
console.log(RegExp.$2); // 1
console.log(RegExp.$3); // 23
console.log(RegExp.$4); // 3
这个正则匹配模式:
第一个字符是数字,比如说1
,
第二个字符是数字,比如说2
,
第三个字符是数字,比如说3
,
接下来的是\1
,是第一个分组内容,第一个开括号对应的分组是123
,
接下来的是\2
,找到第2个开括号,对应的分组,匹配的内容是1
,
接下来的是\3
,找到第3个开括号,对应的分组,匹配的内容是23
,
最后的是\4
,找到第4个开括号,对应的分组,匹配的内容是3
。
3.2 \10表示什么呢?
另外一个疑问可能是,\10
是表示第10
个分组,还是\1
和0
呢?
答案是前者,虽然一个正则里出现\10
比较罕见。测试如下:
let regex = /(1)(2)(3)(4)(5)(6)(7)(8)(9)(#) \10+/;
let test = "123456789# #######";
console.log(regex.test(test)); // true
TIP
如果真要匹配\1
和0
的话,请使用(?:\1)0
或则\1(?:0)
。
3.3 引用不存在的分组会怎样?
因为反向引用,是引用前面的分组,但我们在正则里引用了不存在的分组时,此时正则不会报错,只是匹配反向引用的字符本身。
例如\2
,就匹配"\2"
。注意"\2"
表示对2
进行了转义。
let regex = /\1\2\3\4\5\6\7\8\9/;
console.log(regex.test("\1\2\3\4\5\6\7\8\9")); // true
console.log( "\1\2\3\4\5\6\7\8\9".split("") ); // ["", "", "", "", "", "", "", "8", "9"]
3.4 分组后面有量词会怎么样?
分组后面有量词的话,分组最终捕获到的数据是最后一次的匹配。
let regex = /(\d)+/;
let test = "12345";
console.log(test.match(regex)); // ["12345", "5", index: 0, input: "12345", groups: undefined]
console.log(RegExp.$1); // 5
从上面看出,分组(\d)
捕获的数据是"5"
。
同理对于反向引用,也是这样的。测试如下:
let regex = /(\d)+ \1/;
console.log(regex.test("12345 1")); // false
console.log(regex.test("12345 5")); // true
4、非捕获括号
之前文中出现的括号,都会捕获它们匹配到的数据,以便后续引用,因此也称它们是捕获型分组和捕获型分支。
如果只想要括号最原始的功能,但不会引用它,即,既不在API里医用,也不在正则里反向引用。
此时可以使用非捕获括号(?:p)
和(?:p1|p2|p3)
,例如本文第一个🌰可以修改为:
let regex = /(?:ab)+/g;
let test = "ababa abb ababab";
console.log(test.match(regex)); // ["abab", "ab", "ababab"]
同理,第二个🌰可以修改为:
I love JavaScript
I love Regular Expression
var regex = /^I love (?:JavaScript|Regular Expression)$/;
console.log( regex.test("I love JavaScript") ); // true
console.log( regex.test("I love Regular Expression") ); // true