xsnow
机遇总是留给有准备的人

括号的作用:括号提供了分组,便于我们引用它。

引用某个分组,会有两种情形:在 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个分组,还是\10呢?
答案是前者,虽然一个正则里出现\10比较罕见。测试如下:

let regex = /(1)(2)(3)(4)(5)(6)(7)(8)(9)(#) \10+/;
let test = "123456789# #######";
console.log(regex.test(test)); // true

TIP 如果真要匹配\10的话,请使用(?:\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
posted on 2019-11-13 14:56  xsnow  阅读(2253)  评论(0编辑  收藏  举报