前言
JavaScript 正则表达式方面的文章只需阅读《JavaScript 正则表达式迷你书》即可。本文大部分内容都是该书的读后整理,因此您可以直接阅读该书。作者之所以打算写此文章主要原因是总结总结让自己在平时开发中可以快速查阅。
基础篇
什么是正则
正则表达式 ( Regular Expression ) 是一门简单语言的语法规范,是强大、便捷、高效的文本处理工具,它应用在一些方法中,对字符串中的信息实现查找、替换和提取操作。
# 定义
const reg = /at/g;
# 方法
reg.test('ata'); // 字符串'ata'是否匹配正则reg
正则的作用
正则表达式是匹配模式,要么匹配字符,要么匹配位置。
正则的定义
字面量
const reg = /at/g;
RegExp
const reg = new RegExp('at','g');
元字符
大部分字符在正则表达式中,就是字面的含义,比如 /a/ 匹配 a , /b/ 匹配 b ,但还有一些字符,它们除了字面意思外,还有着特殊的含义,这些字符就是元字符。
元字符 名称 匹配对象
. 点号 单个任意字符(除回车\r、换行\n、行分隔符\u2028和段分隔符\u2029外)
[] 字符组 列出的单个任意字符
[^] 排除型字符组 未列出的单个任意字符
? 问号 匹配0次或1次
* 星号 匹配0次或多次
+ 加号 匹配1次或多次
{min,max} 区间量词 匹配至少min次,最多max次
^ 脱字符 行的起始位置
$ 美元符 行的结束位置
| 竖线 分隔两边的任意一个表达式
() 括号 限制多选结构的范围,标注量词作用的元素,为反向引用捕获文本
\1,\2... 反向引用 匹配之前的第一、第二...组括号内的表达式匹配的文本
转义字符
转义字符表示为反斜线 (\)+ 字符的形式,共有以下3种情况:
-
因为元字符有特殊的含义,所以无法直接匹配。如果要匹配它们本身,则需要在它们前面加上反斜杠
/\*/.test('*'); -
\加非元字符,表示一些不能打印的特殊字符;
| 模式 | 说明 |
|---|---|
\0 | 匹配 NUL 字符 |
\t | 匹配水平制表符 |
\v | 匹配垂直制表符 |
\n | 匹配换行符 |
\r | 匹配回车符 |
\f | 匹配换页符 |
\xnn | 匹配拉丁字符。比如 \xOA 等价于 \n |
\uxxxx | 匹配 Unicode 字符。比如 \u2028 匹配行终止符 |
\cX | 匹配 ctrl + X 。比如 \cI 匹配 ctrl + I,等价于 \t |
[\b] | 匹配 Backspace 键 |
\加任意其他字符,默认情况就是匹配此字符,也就是说,反斜线(\)被忽略了/\x/.test('x')。
双重转义
由于 RegExp 构造函数的参数是字符串,所以某些情况下,需要对字符进行双重转义
var p1 = /\.at/;
//等价于
var p2 = new RegExp('\\.at');
var p1 = /name\/age/;
//等价于
var p2 = new RegExp('name\\/age');
var p1 = /\w\\hello\\123/;
//等价于
var p2 = new RegExp('\\w\\\\hello\\\\123');
[注意] 通常建议使用字面量的方式定义正则。
字符组
字符组就是指用方括号表示的一组字符,它匹配若干字符之一。
[0123456789] 匹配 0-9 这10个数字
[0-9] 匹配 0-9 这10个数字
[a-z] 匹配26个英文字母
[0-9a-zA-Z] 匹配数字大小写字母
排除字符组
字符组的另一个类型是排除型字符组,在左方括号后紧跟一个脱字符'^'表示,表示在当前位置匹配一个没有列出的字符。
[^0-9] 表示除 0-9 以外的字符
| 模式 | 说明 |
|---|---|
[abc] | 匹配"a"、"b"、"c" 其中任何一个字符 |
[a-c1-3] | 匹配"a"、"b"、"c"、1、2、3 其中任何一个字符 |
[^abc] | 匹配除了 "a"、"b"、"c" 之外的任何一个字符 |
[^a-c1-3] | 匹配除了"a"、"b"、"c"、1、2、3 之外的任何一个字符 |
| . | 通配符,匹配除了少数字符(\n)之外的任意字符 |
| \d | 匹配数字,等价于[0-9] |
| \D | 匹配非数字,等价于[^0-9] |
| \w | 匹配单词字符,等价于[a-zA-Z0-9_] |
| \W | 匹配非单词字符,等价于[^a-zA-Z0-9_] |
| \s | 匹配空白符,等价于[ \t\v\n\r\f] |
| \S | 匹配非空白符,等价于[^ \t\v\n\r\f] |
量词
根据字符组的介绍,可以用字符组 [0-9] 或 \d 来匹配单个数字字符,如果用正则表达式表示更复杂的字符串,则不太方便:
//表示邮政编码6位数字
/[0-9][0-9][0-9][0-9][0-9][0-9]/;
或
/\d\d\d\d\d\d/;
正则表达式提供了量词,用来设定某个模式出现的次数
# 表示邮政编码6位数字
/\d{6}/;
| 模式 | 说明 |
|---|---|
{n,m} | 连续出现 n 到 m 次。贪婪模式 |
{n,} | 至少连续出现 n 次。贪婪模式 |
{n} | 连续出现 n 次。贪婪模式 |
| ? | 等价于 {0,1}。贪婪模式 |
| + | 等价于 {1,}。贪婪模式 |
| * | 等价于 {0,}。贪婪模式 |
{n,m}? | 连续出现 n 到 m 次。惰性模式 |
{n,}? | 至少连续出现 n 次。惰性模式 |
{n}? | 连续出现 n 次。惰性模式 |
?? | 等价于 {0,1}?。惰性模式 |
+? | 等价于 {1,}?。惰性模式 |
*? | 等价于{0,}?。惰性模式 |
贪婪模式:默认情况下,量词都是贪婪模式 (greedy quantifier) ,即匹配到下一个字符不满足匹配规则为止。
/a+/.exec('aaa'); // ['aaa']
惰性模式:懒惰模式 (lazy quantifier) 和贪婪模式相对应,在量词后加问号 ? 表示,表示尽可能少的匹配,一旦条件满足就再不往下匹配。
括号的作用
| 模式 | 说明 |
|---|---|
(ab) | 捕获型分组。把"ab"当成一个整体,比如 (ab)+ 表示 "ab" 至少连续出现一次。 |
(?:ab) | 非捕获型分组。与 (ab) 的区别是,它不捕获数据。 |
(good 或 nice) | 捕获型分支。匹配 "good" 或 "nice" |
(?:good 或 nice) | 非捕获型分支结构。与(good 或 nice)的区别是,它不捕获数据。 |
\num | 反向引用。比如 \2,表示引用的是第二个括号里捕获的数据。 |
[注意] (good 或 nice) 由于编译问题它实际应该是 (good|nice)
匹配模式
匹配模式 (match mode) 指匹配时使用的规则。设置特定的模式,可能会改变对正则表达式的识别。
| 符号 | 说明 |
|---|---|
| g | 全局匹配,找到所有满足匹配的子串 |
| i | 匹配过程中,忽略英文字母大小写 |
| m | 多行匹配,把^ 和 $ 变成行开头和行结尾 |
是不是看完以上内容仍然对正则还是云里雾里?不着急读完本文保证可以入门。
应用篇
正则表达式要么匹配字符,要么匹配位置。
推荐两款正则可视化工具:
匹配字符
精确匹配
var regex = /hello/;
console.log( regex.test("hello") ); // true
如果正则只有精确匹配是没多大意义的,比如 /hello/ ,也只能匹配字符串中的 "hello" 这个子串。
模糊匹配
正则表达式之所以强大,是因为其能实现模糊匹配。
而模糊匹配,有两个方向上的“模糊”:横向模糊和纵向模糊。
横向模糊匹配
横向模糊指的是,一个正则可匹配的字符串的长度不是固定的,可以是多种情况的。
const regex = /ab{2,5}c/g;
如图表示:第一个字符是“a”,“b”出现2-5次,最后是字符“c”。
纵向模糊匹配
纵向模糊指的是,一个正则匹配的字符串,具体到某一位字符时,它可以不是某个确定的字符,可以有多种可能。
const regex = /a[123]b/
如图表示:第一个字符是“a”,第二个匹配字符可以是 1,2,3中的一个,第三个字符是“b”;
贪婪匹配
const regex = /\d{2,5}/g;
const string = "123 1234 12345 123456";
console.log( string.match(regex) ); // ["123", "1234", "12345", "12345"]
它是贪婪的,它会尽可能多的匹配。在能力范围内,越多越好。
惰性匹配
惰性匹配,就是尽可能少的匹配。
var regex = /\d{2,5}?/g;
var string = "123 1234 12345 123456";
console.log( string.match(regex) );
// => ["12", "12", "34", "12", "34", "12", "34", "56"]
其中 /\d{2,5}?/ 表示,虽然2到5次都行,当2个就够的时候,就不在往下尝试了。
对惰性匹配的记忆方式是:量词后面加个问号,问一问你知足了吗,你很贪婪吗?
匹配多个子模式
具体形式如下:(p1|p2|p3) ,其中p1、p2和p3是子模式,用 | (管道符)分隔,表示其中任何之一。
var regex = /good|nice/g;
分支结构也是惰性的,即当前面的匹配上了,后面的就不再尝试了。
var string = "goodbye";
var regex1 = /good|goodbye/g; // 匹配good
var regex2 = /goodbye|good/g; // 匹配goodbye
实际案例
匹配手机号码
分析:
13x xxxx xxxx
15x xxxx xxxx
18x xxxx xxxx
正则:
/^1(3|4|5|6|7|8|9)\d{9}$/g
解析:
从上图中我们看到了 Begin 与 End 这两个是匹配开始和结尾位置的,在后面会详细讲解。
- 第一个匹配数字 1;
- 第二个匹配数字 3、4、5、6、7、8、9;
- 第三个匹配任意数字并且出现9次。
匹配位置
什么是位置
位置是相邻字符之间的位置。比如,下图中箭头所指的地方:
如何匹配位置
在ES5中,共有6个锚字符:
^ $ \b \B (?=p) (?!p)
^ 和 &
^(脱字符)匹配开头,在多行匹配中匹配行开头。 $(美元符号)匹配结尾,在多行匹配中匹配行结尾。
比如我们把字符串的开头和结尾用"#"替换(位置可以替换成字符的!):
var result = "hello".replace(/^|$/g, '#'); // "#hello#"
多行匹配模式时,二者是行的概念,这个需要注意:
var result = "I\nlove\njavascript".replace(/^|$/gm, '#');
// 输出如下:
#I#
#love#
#javascript#
\b 和 \B
\b 是单词边界,具体就是:
\w和\W之间的位置;\w和^之间的位置;\w和$之间的位置。
var result = "[JS] Lesson_01.mp4".replace(/\b/g, '#');
// => "[#JS#] #Lesson_01#.#mp4#"
- 第一个
"#",两边是"["与"J",是\W和\w之间的位置; - 第二个
"#",两边是"S"与"]",也就是\w和\W之间的位置; - 第三个
"#",两边是空格与"L",也就是\W和\w之间的位置; - 第四个
"#",两边是"1"与".",也就是\w和\W之间的位置; - 第五个
"#",两边是"."与"m",也就是\W和\w之间的位置; - 第六个
"#",其对应的位置是结尾,但其前面的字符"4"是\w,即\w和$之间的位置。
知道了 \b 的概念后,那么 \B 也就相对好理解了。
\B 就是 \b 的反面的意思,非单词边界。例如在字符串中所有位置中,扣掉 \b ,剩下的都是 \B 的。
具体说来就是:
\w与\w之间的位置;\W与\W之间的位置;^与\W之间的位置;\W与$之间的位置。
var result = "[JS] Lesson_01.mp4".replace(/\B/g, '#');
// => "#[J#S]# L#e#s#s#o#n#_#0#1.m#p#4"
(?=p) 和 (?!p)
(?=p) ,其中 p 是一个子模式,即 p 前面的位置。
比如 (?=l) ,表示 'l' 字符前面的位置,例如:
var result = "hello".replace(/(?=l)/g, '#');
// => "he#l#lo"
而 (?!p) 就是 (?=p) 的反面意思,表示接下来不能是 p 的位置。
var result = "hello".replace(/(?!l)/g, '#');
// => "#h#ell#o#"
二者的学名是:
(?=p) 正向先行断言
(?!p) 负向先行断言
ES6中,还支持 positive lookbehind 和 negative lookbehind,具体是 (?<=p) 和 (?<!p) 。
位置的特性
对于位置的理解,我们可以理解成空字符 "" 。
比如 "hello" 字符串等价于如下的形式:
"hello" == "" + "h" + "" + "e" + "" + "l" + "" + "l" + "o" + "";
等价于:
"hello" == "" + "" + "hello"
因此,把 /^hello$/ 写成 /^^hello?$/ ,是没有任何问题的:
var result = /^^hello?$/.test("hello"); // => true
把位置理解空字符,是对位置非常有效的理解方式。
实际案例
数字千分位格式化
分析:
12345678 => 12,345,678
123456789 => 123,456,789
第一版正则:
var result = "12345678".replace(/(?=\d{3}$)/g, ',')
解析:
(?=\d{3})匹配三个数字前面的位置,它会从字符串后到前的找",1,2,3,4,5,678"(?=\d{3}$)匹配结束位置并且前面三个数字的那个位置"12345,678"
第一版正则只匹配了一次。
第二版正则:
var result = "12345678".replace(/(?=(\d{3})+$)/g, ',')
解析:
- 匹配结束位置并且每隔3个数字都会添加一个逗号
12,345,678; - 但是当数字为9位数字时,正则又出问题了,最前面会增加一个逗号
,123,456,789。
聪明的你肯定想到了,不匹配开头位置即可。
第三版正则:
var result = "12345678".replace(/(?!^)(?=(\d{3})+$)/g, ',')
这次就能产生正确的结果了
123,456,789 。
括号的作用
括号的作用,其实三言两语就能说明白,括号提供了分组,便于我们引用它。
引用某个分组,会有两种情形:在 JavaScript 里引用它,在正则表达式里引用它。
分组
var regex = /(ab)+/g;
其中括号是提供分组功能,使量词 + 作用于
“ab” 这个整体
分支结构
而在多选分支结构 (p1|p2) 中,此处括号的作用也是不言而喻的,提供了子表达式的所有可能。
var regex = /^I love (JavaScript|Regular Expression)$/;
var regex2 = /^I love JavaScript|Regular Expression$/;
可以想想这两个正则的区别:
图中可以轻易看出两者的区别。
引用分组
这是括号一个重要的作用,有了它,我们就可以进行数据提取,以及更强大的替换操作。 而要使用它带来的好处,必须配合使用实现环境的API。
比如提取出年、月、日,可以这么做:
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
console.log( string.match(regex) );
// => ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]
比如,想把 yyyy-mm-dd 格式,替换成 mm/dd/yyyy 怎么做?
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, "$2/$3/$1");
console.log(result);
// => "06/12/2017"
或者这样写:
var result = string.replace(regex, function() {
return RegExp.$2 + "/" + RegExp.$3 + "/" + RegExp.$1;
});
或者:
var result = string.replace(regex, function(match, year, month, day) {
return month + "/" + day + "/" + year;
});
反向引用
除了使用相应 API 来引用分组,也可以在正则本身里引用分组。但只能引用之前出现的分组,即反向引用。
var regex = /\d{4}(-|\/|\.)\d{2}\1\d{2}/;
var string1 = "2017-06-12";
var string2 = "2017/06/12";
var string3 = "2017.06.12";
var string4 = "2016-06/12";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // true
console.log( regex.test(string4) ); // false
注意里面的 \1 ,表示的引用之前的那个分组 (-|\/|\.) 。不管它匹配到什么(比如 - ), \1 都匹配那个同样的具体某个字符。
我们知道了 \1 的含义后,那么 \2 和 \3 的概念也就理解了,即分别指代第二个和第三个分组。
引用不存在的分组会怎样?
因为反向引用,是引用前面的分组,但我们在正则里引用了不存在的分组时,此时正则不会报错,只是匹配反向引用的字符本身。例如 \2 ,就匹配 "\2" 。注意 "\2" 表示对 "2" 进行了转意。
非捕获分组
之前文中出现的分组,都会捕获它们匹配到的数据,以便后续引用,因此也称他们是捕获型分组。
如果只想要括号最原始的功能,但不会引用它,即,既不在 API 里引用,也不在正则里反向引用。此时可以使用非捕获分组 (?:p) ,例如本文第一个例子可以修改为:
var regex = /(?:ab)+/g;
var string = "ababa abbb ababab";
console.log( string.match(regex) );
// => ["abab", "ab", "ababab"]
实际案例
匹配成对的标签
分析:
# 匹配
<title>regular expression</title>
<p>laoyao bye bye</p>
# 不匹配
<title>wrong!</p>
正则:
var regex = /<([^>]+)>[\d\D]*<\/\1>/;
var string1 = "<title>regular expression</title>";
var string2 = "<p>laoyao bye bye</p>";
var string3 = "<title>wrong!</p>";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // false
正则表达式拆分
对于一门语言的掌握程度怎么样,可以有两个角度来衡量:读和写。
不仅要求自己能解决问题,还要看懂别人的解决方案。代码是这样,正则表达式也是这样。
结构和操作符
编程语言一般都有操作符。只要有操作符,就会出现一个问题。当一大堆操作在一起时,先操作谁,又后操作谁呢?为了不产生歧义,就需要语言本身定义好操作顺序,即所谓的优先级。
JS正则表达式中,都有哪些结构呢?
字面量
匹配一个具体字符,包括不用转义的和需要转义的。比如 a 匹配字符 "a" ,又比如 \n 匹配换行符,又比如 \. 匹配小数点。
字符组
匹配一个字符,可以是多种可能之一,比如 [0-9] ,表示匹配一个数字。也有 \d 的简写形式。另外还有反义字符组,表示可以是除了特定字符之外任何一个字符,比如 [^0-9] ,表示一个非数字字符,也有 \D 的简写形式。
量词
表示一个字符连续出现,比如 a{1,3} 表示 “a” 字符连续出现3次。另外还有常见的简写形式,比如 a+ 表示 “a” 字符连续出现至少一次。
锚点
匹配一个位置,而不是字符。比如^匹配字符串的开头,又比如 \b 匹配单词边界,又比如 (?=\d) 表示数字前面的位置。
分组
用括号表示一个整体,比如 (ab)+ ,表示 "ab" 两个字符连续出现多次,也可以使用非捕获分组 (?:ab)+ 。
分支
多个子表达式多选一,比如 abc|bcd ,表达式匹配 "abc" 或者 "bcd" 字符子串。
反向引用
比如 \2 ,表示引用第2个分组。
其中涉及到的操作符有:
1.转义符 \
2.括号和方括号 (...)、(?:...)、(?=...)、(?!...)、[...]
3.量词限定符 {m}、{m,n}、{m,}、?、*、+
4.位置和序列 ^ 、$、 \元字符、 一般字符
5. 管道符(竖杠)|
上面操作符的优先级从上至下,由高到低。
这里,我们来分析一个正则: /ab?(c|de*)+|fg/
- 由于括号的存在,所以,
(c|de*)是一个整体结构。 - 在
(c|de*)中,注意其中的量词*,因此e*是一个整体结构。 - 又因为分支结构
“|”优先级最低,因此c是一个整体、而de*是另一个整体。 - 同理整个正则分成了
a、b?、(...)+、f、g。而由于分支的原因,又可以分成ab?(c|de*)+和fg这两部分。
量词连缀问题
假设,要匹配这样的字符串:
- 每个字符为
a、b、c任选其一 - 字符串的长度是3的倍数
此时正则不能想当然地写成 /^[abc]{3}+$/ ,这样会报错,说 + 前面没什么可重复的:
此时要修改成:/^([abc]{3})+$/
实际案例
IPV4地址
正则:
/^((0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])\.){3}(0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])$/
拆解:
- 首先宏观观察会得出如下结构:
((...)\.){3}(...); - 并且
(...)里面的结构是一致的,接下来拆解里面的正则; 0{0,2}\d,匹配一位数,包括0补齐的。比如,9、09、009;0?\d{2},匹配两位数,包括0补齐的,也包括一位数;1\d{2},匹配100-199;2[0-4]\d,匹配200-249;25[0-5],匹配250-255。
因此当你发觉拆解困难时,可以借助图形化帮助我们完成此项工作,但是我们也依旧需要了解拆解原理。
原理篇
学习正则表达式,是需要懂点儿匹配原理的。 而研究匹配原理时,有两个字出现的频率比较高:“回溯”。
回溯法
正则表达式匹配字符串的这种方式,有个学名,叫回溯法。
回溯法也称试探法,它的基本思想是:从问题的某一种状态(初始状态)出发,搜索从这种状态出发所能达到的所有“状态”,当一条路走到“尽头”的时候(不能再前进),再后退一步或若干步,从另一种可能“状态”出发,继续搜索,直到所有的“路径”(状态)都试探过。这种不断“前进”、不断“回溯”寻找解的方法,就称作“回溯法”。
本质上就是深度优先搜索算法。其中退到之前的某一步这一过程,我们称为“回溯”。从上面的描述过程中,可以看出,路走不通时,就会发生“回溯”。即,尝试匹配失败时,接下来的一步通常就是回溯。
没有回溯的匹配
假设我们的正则是 /ab{1,3}c/ ,其可视化形式是:
而当目标字符串是
"abbbc" 时,就没有所谓的“回溯”。其匹配过程是:
其中子表达式
b{1,3} 表示 “b” 字符连续出现1到3次。
有回溯的匹配
如果目标字符串是 "abbc" ,中间就有回溯。
图中第5步有红颜色,表示匹配不成功。此时
b{1,3} 已经匹配到了2个字符 “b” ,准备尝试第三个时,结果发现接下来的字符是 “c” 。那么就认为 b{1,3} 就已经匹配完毕。然后状态又回到之前的状态(即第6步,与第4步一样),最后再用子表达式 c,去匹配字符 “c” 。当然,此时整个表达式匹配成功了。
图中的第6步,就是“回溯”。
再看一个清晰的回溯,正则是:
目标字符串是:
"acd"ef ,匹配过程是:
图中省略了尝试匹配双引号失败的过程。可以看出
.* 是非常影响效率的。
为了减少一些不必要的回溯,可以把正则修改为 /"[^"]*"/ 。
正则优化
正则表达式的运行分为如下的阶段:
- 编译
- 设定起始位置
- 尝试匹配
- 匹配失败的话,从下一位开始继续第3步
- 最终结果:匹配成功或失败
使用具体型字符组来代替通配符,来消除回溯
在第三阶段,最大的问题就是回溯。
例如,匹配双引用号之间的字符。如,匹配字符串 123"abc"456 中的 "abc" 。
如果正则用的是: /".*"/ ,会在第3阶段产生4次回溯(粉色表示 .* 匹配的内容):
如果正则用的是: /".*?"/ ,会产生2次回溯(粉色表示 .*? 匹配的内容):
因为回溯的存在,需要引擎保存多种可能中未尝试过的状态,以便后续回溯时使用。注定要占用一定的内存。
此时要使用具体化的字符组,来代替通配符.,以便消除不必要的字符,此时使用正则 /"[^"]*"/ ,即可。
使用非捕获型分组
因为括号的作用之一是,可以捕获分组和分支里的数据。那么就需要内存来保存它们。
当我们不需要使用分组引用和反向引用时,此时可以使用非捕获分组。例如:
/^[+-]?(\d+\.\d+|\d+|\.\d+)$/
可以修改成:
/^[+-]?(?:\d+\.\d+|\d+|\.\d+)$/
独立出确定字符
例如 /a+/ ,可以修改成 /aa*/ 。
因为后者能比前者多确定了字符 a 。这样会在第四步中,加快判断是否匹配失败,进而加快移位的速度。
提取分支公共部分
比如 /^abc|^def/ ,修改成 /^(?:abc|def)/ 。
又比如 /this|that/ ,修改成 /th(?:is|at)/ 。
这样做,可以减少匹配过程中可消除的重复。
减少分支的数量,缩小它们的范围
/red|read/ ,可以修改成 /rea?d/ 。此时分支和量词产生的回溯的成本是不一样的。但这样优化后,可读性会降低的。
小结
最后再次强调,本文只是方便作者自己阅读,请阅读原著《JavaScript 正则表达式迷你书》。