RegExp 语法
元字符
元字符(Meta-Character) 指那些在正则表达式中具有特殊意义的专用字符,可以用来规定其前导字符(即位于元字符前面的字符)在目标对象中的出现模式。
| 元字符 | 名称 | 匹配对象 | 
|---|---|---|
| . | 点号 | 单个任意字符(除回车 \r、换行\n、行分隔符\u2028和段分隔符\u2029外) | 
| [] | 字符组 | 列出的单个任意字符 | 
| [^] | 排除型字符组 | 未列出的单个任意字符 | 
| ? | 问号 | 匹配 0 次或 1 次 | 
| * | 星号 | 匹配 0 次或多次 | 
| + | 加号 | 匹配 1 次或多次 | 
| {min,max} | 区间量词 | 匹配至少 min 次,最多 max 次 | 
| ^ | 脱字符 | 行的起始位置 | 
| $ | 美元符 | 行的结束位置 | 
| ` | ` | 竖线 | 
| () | 括号 | 限制多选结构的范围,标注量词作用的元素,为反向引用捕获文本 | 
| \1,\2... | 反向引用 | 匹配之前的第一、第二...组括号内的表达式匹配的文本 | 
字符类别
字符组简记
用 [0-9]、[a-z] 等字符组,可以很方便地表示数字字符和小写字母字符。对于这类常用字符组,正则表达式提供了更简单的记法,这就是字符组简记(Shorthands)。
常见的字符组简记有 \d、\w、\s,其中:
- d表示(Digit)数字
- w表示(Word)单词
- s表示(Space)空白
正则表达式也提供了对应排除型字符组的简记法:\D、\W、\S。字母完全相同,只是改为大写,这些简记法匹配的字符互补。
| 字符 | 描述 | 
|---|---|
| \d | 数字,等同于 [0-9] | 
| \D | 非数字,等同于 [^0-9] | 
| \s | 空白字符,等同于 [\f\n\r\t\u000B\u0020\u00A0\u2028\u2029] | 
| \S | 非空白字符,等同于 [^\f\n\r\t\u000B\u0020\u00A0\u2028\u2029] | 
| \w | 字母、数字、下划线,等同于 [0-9A-Za-z_] | 
| \W | 非字母、数字、下划线,等同于 [^0-9A-Za-z_] | 
任意字符
| 字符 | 描述 | 
|---|---|
| . | 表示除回车 (\r)、换行(\n)、行分隔符(\u2028)和段分隔符(\u2029)以外的任意字符。 | 
⚠️ 注意:一般认为点号可以代表任意字符,其实并不是
妥善的利用互补属性,可以得到一些巧妙的效果。比如,
[\s\S]、[\w\W]、[\d\D]都可以表示任意字符。
匹配任意字符
/./.test('\r');
// false
/[\s\S]/.test('\r');
// true
转义字符
转义字符(Escape) 表示为反斜线 \ 加字符的形式,共有以下 3 种情况。
| 字符 | 描述 | 
|---|---|
| \+ 元字符 | 匹配元字符 | 
| \+]或\+} | 右方括号和右花括号无需转义 | 
| \+ 非元字符 | 表示一些不能打印的特殊字符 | 
| \+ 除上述其他字符 | 默认情况匹配此字符 | 
<br />
因为元字符有特殊的含义,所以无法直接匹配。如果要匹配它们本身,则需要在它们前面加上反斜杠 \。
/1+1/.test('1+1');
// false
/1\+1/.test('1+1');
// true
/\*/.test('*');
// true
/*/.test('*');
// 报错
但实际上,并非 14 个元字符都需要转义,右方括号 ] 和右花括号 } 不需要转义
/]/.test(']');
// true
/\]/.test(']');
// true
/\}/.test('}');
// true
/}/.test('}');
// true
\ 加非元字符,表示一些不能打印的特殊字符。
<br />
| 字符 | 描述 | 
|---|---|
| \0 | NUL 字符 \u0000 | 
| [\b] | 匹配退格符 \u0008,不要与\b混淆 | 
| \t | 制表符 \u0009 | 
| \n | 换行符 \u000A | 
| \v | 垂直制表符 \u000B | 
| \f | 换页符 \u000C | 
| \r | 回车符 \u000D | 
| \xnn | 由十六进制数 nn指定的拉丁字符 | 
| \uxxxx | 由十六进制数 xxxx指定的 Unicode 字符(\u4e00-\u9fa5代表中文) | 
| \cX | 控制字符 ^X,表示ctrl-[X],其中的 X 是 A-Z 之中任一个英文字母,用来匹配控制字符 | 
\ 加任意其他字符,默认情况就是匹配此字符,也就是说,反斜线 (\) 被忽略了。
/\x/.test('x');
// true
/\y/.test('y');
// true
/\z/.test('z');
// true
双重转义
由于 RegExp 构造函数的参数是字符串,所以某些情况下,需要对字符进行 双重转义。
字符 \ 在正则表达式字符串中通常被转义为 \\ 。
const reg1 = /\.at/;
// 等价于
const reg2 = new RegExp('\\.at');
const reg3 = /name\/age/;
// 等价于
const reg4 = new RegExp('name\\/age');
const reg5 = /\w\\hello\\123/;
// 等价于
const reg6 = new RegExp('\\w\\\\hello\\\\123');
字符集合
字符集合(Character Sets),有的编译成字符类或字符集。简单而言,就是指用方括号表示的一组字符,它匹配若干字符之一。
| 字符 | 描述 | 
|---|---|
| [xyz] | 一个字符集合,也叫字符组。匹配集合中任意一个字符。可以使用 -指定一个范围。 | 
| [^xyz] | 一个反义或补充字符集,也叫反义字符组。匹配任意不包括括号内的字符。可以使用 -指定一个范围。 | 
<br />
// 匹配 0-9 这 10 个数字之一
const regexp = /[0123456789]/;
regexp.test('1');
// true
regexp.test('a');
// false
字符组中的字符排列顺序并不影响字符组的功能,出现重复字符也不会影响。
以下三个表达式都是相等的。
const regexp1 = /[0123456789]/;
const regexp2 = /[9876543210] /;
const regexp3 = /[1234567890123456789]/;
范围
正则表达式通过连字符 (-) 提供了范围表示法,可以简化字符组
const regexp1 = /[0123456789]/;
// 等价于
const regexp2 = /[0-9]/;
const regexp3 = /[abcdefghijklmnopqrstuvwxyz]/;
// 等价于
const regexp4 = /[a-z]/;
连字符 (-) 表示的范围是根据 ASCII 编码的码值来确定的,码值小的在前,码值大的在后。

所以 [0-9] 是合法的,而 [9-0] 会报错。
//匹配 0-9 这 10 个数字之一
const regexp1 = /[0-9]/;
regexp1.test('1');
// true
const regexp2 = /[9-0]/;
// 报错
regexp2.test('1');
在字符组中可以同时并列多个 - 范围。
const regexp1 = /[0-9a-zA-Z]/;
// 匹配数字、大写字母和小写字母
const regexp2 = /[0-9a-fA-F]/;
// 匹配数字,大、小写形式的a-f,用来验证十六进制字符
const regexp3 = /[0-9a-fA-F]/.test('d');
// true
const regexp4 = /[0-9a-fA-F]/.test('x');
// false
只有在字符组内部,连字符 - 才是元字符,表示一个范围,否则它就只能匹配普通的连字符号。
如果连字符出现在字符组的开头或末尾,它表示的也是普通的连字符号,而不是一个范围。
// 匹配中划线
/-/.test('-');
// true
/[-]/.test('-');
// true
// 匹配0-9的数字或中划线
/[0-9]/.test('-');
// false
/[0-9-]/.test('-');
// true
/[0-9\-]/.test('-');
// true
/[-0-9]/.test('-');
// true
/[\-0-9]/.test('-');
// true
排除
字符组的另一个类型是 排除型字符组,在左方括号后紧跟一个脱字符 ^ 表示,表示在当前位置匹配一个没有列出的字符。
所以 [^0-9] 表示 0-9 以外的字符。
// 匹配第一个是数字字符,第二个不是数字字符的字符串
/[0-9][^0-9]/.test('1e');
// true
/[0-9][^0-9]/.test('q2');
// false
在字符组内部,脱字符 ^ 表示排除,而在字符组外部,脱字符 ^ 表示一个行锚点。
^ 符号是元字符,在字符组中只要 ^ 符号不挨着左方括号就可以表示其本身含义,不转义也可以。
// 匹配 abc 和 ^ 符号
/[a-c^]/.test('^');
// true
/[a-c\^]/.test('^');
// true
/[\^a-c]/.test('^');
// true
在字符组中,只有 ^ 、 - 、[ 、] 这 4 个字符可能被当做元字符,其他有元字符功能的字符都只表示其本身。
/[[1]]/.test('[');
// false
/[[1]]/.test(']');
// false
/[\1]/.test('\\');
// false
/[^^]/.test('^');
// false
/[1-2]/.test('-');
// false
/[\[1\]]/.test('[');
// true
/[\[1\]]/.test(']');
// true
/[\\]/.test('\\');
// true
/[^]/.test('^');
// true
/[1-2\-]/.test('-');
// true
数量词
正则表达式提供了量词,用来设定某个模式出现的次数。
| 字符 | 描述 | 
|---|---|
| x* | 相当于 x{0,}(匹配任意多次) | 
| x+ | 相当于 x{1,}(匹配至少一次) | 
| x? | 相当于 x{0,1}(不匹配或匹配一次) | 
| x*?或x+? | 相当于 *和+字符,然而匹配的是最小可能匹配 | 
| x(?=y) | 只有当 x后面紧跟着y时,才匹配x。(了解详情请看 环视) | 
| x(?!y) | 只有当 x后面不是紧跟着y时,才匹配x。(了解详情请看 环视) | 
| x|y(这里是没有\的) | 匹配 x或y | 
| x{n} | 匹配 n次(n为正整数) | 
| x{n,m} | 匹配至少 n次,最多m次(n和m为正整数) | 
| x{n,} | 匹配至少 n次(n为正整数) | 
<br />
邮政编码
// 表示邮政编码 6 位数字
const regexp = /\d{6}/;
美国英语和英国英语有些词的写法不一样,如果 traveler 和 traveller,favor 和 favour,color 和 colour。
// 同时匹配美国英语和英国英语单词
const regexp1 = /travell?er/;
const regexp2 = /favou?r/;
const regexp3 = /colou?r/;
协议名有 HTTP 和 HTTPS 两种:
const regexp1 = /https?/;
选择
竖线 | 在正则表达式中表示或关系的选择,以竖线 | 分隔开的多个子表达式也叫选择分支或选择项。在一个选择结构中,选择分支的数目没有限制。
在选择结构中,竖线 | 用来分隔选择项,而括号 () 用来规定整个选择结构的范围。如果没有出现括号,则将整个表达式视为一个选择结构。
选择项的尝试匹配次序是从左到右,直到发现了匹配项,如果某个选择项匹配就忽略右侧其他选择项,如果所有子选择项都不匹配,则整个选择结构匹配失败。
/12|23|34/.exec('1');
// null
/12|23|34/.exec('12');
// ['12']
/12|23|34/.exec('23');
// ['23']
/12|23|34/.exec('2334');
// ['23']
IP 地址一般由 3 个点号和 4 段数字组成,每段数字都在 0-255 之间。
- 0-199:[01]?\d\d?
- 200-249:2[0-4]\d
- 250-255:25[0-5]
IP 地址:
const ipRegExp = /((2[0-4]\d|25[0-5]|[0-1]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[0-1]?\d\d?)/;
ipRegExp.test('1.1.1.1');
// true
ipRegExp.test('1.1.1');
// false
ipRegExp.test('256.1.1.1');
// false
类似地,时间匹配也需要分段处理:
// 月(1-12)
0?\d|1[0-2]
// 日(1-31)
0?\d|[12]\d|3[01]
// 小时(0-24)
0?\d|1\d|2[0-4]
// 分钟(0-60)
0?\d|[1-5]\d|60
手机号一般是 11 位,前 3 位是号段,后 8 位一般没有限制。而且,在手机开头很可能有 0 或+86。
- 开头:(0|\+86)?
- 前 3 位:13\d|14[579]|15[0-35-9]|17[0135-8]|18\d
- 后 8 位:\d{8}
const phone = /(0|\+86)?(13\d|14[579]|15[0-35-9]|17[0135-8]|18\d)\d{8}/;
phone.test('13453250661');
// true
phone.test('1913250661');
// false
phone.test('1345325061');
// false
在选择结构中,应该尽量避免选择分支中存在重复匹配,因为这样会大大增加回溯的计算量
// 错误示范 🙅♂️
const regexp = /a|[ab][0-9]|\w/;
贪婪模式
默认情况下,量词都是贪婪模式(Greedy quantifier),即匹配到下一个字符不满足匹配规则为止。
// exec 方法以数组的形式返回匹配结果
/a+/.exec('aaa');
// ['aaa']
懒惰模式
懒惰模式(Lazy quantifier) 和贪婪模式相对应,在量词后加问号 ? 表示,表示尽可能少的匹配,一旦条件满足就再不往下匹配。
| 符号 | 释义 | 
|---|---|
| {n}? | 匹配 n次 | 
| {n,m}? | 匹配至少 n次,最多m次 | 
| {n,}? | 匹配至少 n次 | 
| ?? | 相当于 {0,1} | 
| *? | 相当于 {0,} | 
| +? | 相当于 {1,} | 
<br />
示例:
/a+?/.exec('aaa');
// ['a']
匹配 <script></script> 之间的代码看上去很容易
const regexp = /<script>[\s\S]*<\/script>/;
regexp.exec('<script>alert("1");</script>');
// ["<script>alert("1");</script>"]
但如果多次出现 script 标签,就会出问题
const regexp = /<script>[\s\S]*<\/script>/;
regexp.exec('<script>alert("1");</script><br><script>alert("2");</script>');
// ["<script>alert("1");</script><br><script>alert("2");</script>"]
它把无用的 <br> 标签也匹配出来了,此时就需要使用懒惰模式
const regexp = /<script>[\s\S]*?<\/script>/;
regexp.exec('<script>alert("1");</script><br><script>alert("2");</script>');
// ["<script>alert("1");</script>"]
在 JavaScript 中,/* */ 是注释的一种形式,在文档中可能出现多次,这时就必须使用懒惰模式
const regexp = /\/\*[\s\S]*?\*\//;
regexp.exec('/*abc*/<br>/*123*/');
// ["/*abc*/"]
分组与反向引用
分组
量词控制之前元素的出现次数,而这个元素可能是一个字符,也可能是一个字符组,也可以是一个表达式。
如果把一个表达式用括号包围起来,这个元素就是括号里的表达式,被称为 子表达式。
示例 1:如果希望字符串 ab 重复出现 2 次,应该写为 (ab){2},而如果写为 ab{2},则 {2} 只限定 b。
/(ab){2}/.test('abab');
// true
/(ab){2}/.test('abb');
// false
/ab{2}/.test('abab');
// false
/ab{2}/.test('abb');
// true
示例 2:身份证长度有 15 位和 18 位两种,如果只匹配长度,可能会想当然地写成 \d{15,18},实际上这是错误的,因为它包括 15、16、17、18 这四种长度。
// 正确写法
var idCard = /\d{15}(\d{3})?/;
示例 3:Email 地址以 @ 分隔成两段,之前的部分是用户名,之后的部分是主机名。
用户名允许出现数字、字母和下划线,长度一般在 1-64 个字符之间,则正则可表示为 /\w{1,64}/
主机名一般表现为 a.b.···.c,其中 c 为主域名,其他为级数不定的子域名,则正则可表示为 /([-a-zA-z0-9]{1,63}\.)+[-a-zA-Z0-9]{1,63}/
所以 email 地址的正则表达式如下:
const email = /\w{1,64}@([-a-zA-z0-9]{1,63}\.)+[-a-zA-Z0-9]{1,63}/;
email.test('q@qq.com');
// true
email.test('q@qq');
// false
email.test('q@a.qq.com');
// true
捕获
括号不仅可以对元素进行分组,还会保存每个分组匹配的文本,等到匹配完成后,引用捕获的内容。因为捕获了文本,这种功能叫 捕获分组。
比如,要匹配诸如 2016-06-23 这样的日期字符串
const regexp = /(\d{4})-(\d{2})-(\d{2})/;
与以往不同的是,年、月、日这三个数值被括号括起来了,从左到右为第 1 个括号、第 2 个括号和第 3 个括号,分别代表第 1、2、3 个捕获组。
JavaScript 有 9 个用于存储捕获组的构造函数属性。
RegExp.$1、RegExp.$2、RegExp.$3 到 RegExp.$9 分别用于存储第一、第二第九个匹配的捕获组。
在调用 exec() 或 test() 方法时,这些属性会被自动填充。
/(\d{4})-(\d{2})-(\d{2})/.test('2016-06-23');
// true
console.log(RegExp.$1);
// '2016'
console.log(RegExp.$2);
// '06'
console.log(RegExp.$3);
// '23'
console.log(RegExp.$4);
// ''
而 exec() 方法是专门为捕获组而设计的,返回的数组中,第一项是与整个模式匹配的字符串,其他项是与模式中的捕获组匹配的字符串。
/(\d{4})-(\d{2})-(\d{2})/.exec('2016-06-23');
// ["2016-06-23", "2016", "06", "23"]
捕获分组捕获的文本,不仅可以 用于数据提取,也可以 用于替换。
replace() 方法就是用于进行数据替换的,该方法接收两个参数,第一个参数为待查找的内容,而第二个参数为替换的内容。
'2000-01-01'.replace(/-/g, '.');
// 2000.01.01
在 replace() 方法中也可以引用分组,形式是 $num, num 是对应分组的编号。
把 2000-01-01 的形式变成 01-01-2000 的形式:
'2000-01-01'.replace(/(\d{4})-(\d{2})-(\d{2})/g, '$3-$2-$1');
//'01-01-2000'
反向引用
英文中不少单词都有重叠出现的字母,如 shoot 或 beep。若想检查某个单词是否包含重叠出现的字母,则需要引入 反向引用(back-reference)
反向引用允许在正则表达式内部引用之前捕获分组匹配的文本,形式是 \num,num 表示所引用分组的编号。
//重复字母
/([a-z])\1/
/([a-z])\1/.test('aa');
// true
/([a-z])\1/.test('ab');
// false
反向引用可以用于建立前后联系。HTML 标签的开始标签和结束标签是对应的。
// 开始标签
const startIndex = /<([^>]+)>/
// 标签内容
const content = /[\s\S]*?/
// 匹配成对的标签
const couple = /<([^>]+)>[\s\S]*?<\/\1>/
/<([^>]+)>[\s\S]*?<\/\1>/.test('<a>123</a>');
// true
/<([^>]+)>[\s\S]*?<\/\1>/.test('<a>123</b>');
// false
非捕获
除了捕获分组,正则表达式还提供了 非捕获分组(non-capturing group),以 (?:) 的形式表示,它只用于限定作用范围,而不捕获任何文本。
比如,要匹配 abcabc 这个字符,一般地,可以写为 (abc){2},但由于并不需要捕获文本,只是限定了量词的作用范围,所以应该写为 (?:abc){2}。
/(abc){2}/.test('abcabc');
// true
/(?:abc){2}/.test('abcabc');
// true
由于非捕获分组不捕获文本,对应地,也就没有捕获组编号。
/(abc){2}/.test('abcabc');
// true
console.log(RegExp.$1);
// 'abc'
/(?:abc){2}/.test('abcabc');
// true
console.log(RegExp.$1);
// ''
非捕获分组也不可以使用反向引用。
/(?:123)\1/.test('123123');
// false
/(123)\1/.test('123123');
// true
捕获分组和非捕获分组可以在一个正则表达式中同时出现。
/(\d)(\d)(?:\d)(\d)(\d)/.exec('12345');
// ["12345", "1", "2", "4", "5"]
断言
在正则表达式中,有些结构并不真正匹配文本,而只负责判断在某个位置左/右侧是否符合要求,这种结构被称为 断言(assertion),也称为 锚点(anchor),常见的断言有 3 种:
- 单词边界
- 起始结束
- 环视
单词边界
在文本处理中可能会经常进行单词替换,比如把 row 替换成 line。但是,如果直接替换,不仅所有单词 row 都被替换成 line,单词内部的 row 也会被替换成 line。要想解决这个问题,必须有办法确定单词 row,而不是字符串 row。
为了解决这类问题,正则表达式提供了专用的 单词边界(word boundary),记为 \b ,它匹配的是 单词边界 的位置,而不是字符。\b 匹配的是一边是单词字符 \w ,一边是非单词字符 \W 的位置
与 \b 对应的还有 \B,表示非单词边界,但实际上 \B 很少使用
/\ban\b/.test('an apple');
// true
/\ban\b/.test('a an');
// true
/\ban\b/.test('an');
// true
/\ban\b/.test('and');
// false
/\ban\b/.test('ban');
// false
起始结束
常见的断言还有 ^ 和 $,它们分别匹配字符串的开始位置和结束位置,所以可以用来判断整个字符串能否由表达式匹配。
//匹配第一个单词
/^\w*/.exec('first word\nsecond word\nthird word');
// ['first']
//匹配最后一个单词
/\w*$/.exec('first word\nsecond word\nthird word');
// ['word']
/^a$/.test('a\n');
// false
/^a$/.test('a');
// true
^ 和 $ 的常用功能是删除字符串首尾多余的空白,类似于字符串 String 对象的 trim() 方法。
function fnTrim(str) {
  str.replace(/^\s+|\s+$/, '');
}
console.log(fnTrim('      hello world   '));
// 'hello world'
环视
环视(Look-around),可形象地解释为停在原地,四处张望。环视类似于单词边界,在它旁边的文本需要满足某种条件,而且本身不匹配任何字符。
环视分为 正序环视 和 逆序环视,而 JavaScript 只支持正序环视,相当于只支持向前看,不支持往回看。
而正序环视又分为 肯定正序环视 和 否定正序环视。
| 符号 | 描述 | 
|---|---|
| x(?=y) | 肯定 正序环视,表示 x后紧跟着y才匹配 | 
| x(?!y) | 否定 正序环视,表示 x后不紧跟着y才匹配 | 
<br />
/a(?=b)/.exec('abc');
// ['a']
/a(?=b)/.exec('ac');
// null
/a(?!b)/.exec('abc');
// null
/a(?!b)/.exec('ac');
// ['a']
/a(?=b)b/.exec('abc');
// ['ab']
环视虽然也用到括号,却与捕获型分组编号无关;但如果环视结构出现捕获型括号,则会影响分组。
/ab(?=cd)/.exec('abcd');
// ['ab']
/ab(?=(cd))/.exec('abcd');
// ['ab','cd']
匹配模式
匹配模式(Match Mode) 指匹配时使用的规则。设置特定的模式,可能会改变对正则表达式的识别。
不区分大小写模式
默认地,正则表达式是 区分大小写 的,通过设置标志 i,可以 忽略大小写(ignore case)。
/ab/.test('aB');
// false
/ab/i.test('aB');
// true
多行模式
默认地,正则表达式中的 ^ 和 $ 匹配的是整个字符串的起始位置和结束位置,而通过设置标志 m,开启多行模式,它们也能匹配字符串内部某一行文本的起始位置和结束位置。
// example 1
/world$/.test('hello world\n');
// false
/world$/m.test('hello world\n');
// true
// example 2
/^b/.test('a\nb');
// false
/^b/m.test('a\nb');
// true
全局模式
默认地,第一次匹配成功后,正则对象就停止向下匹配了。g 修饰符表示 全局匹配(global),设置 g 标志后,正则对象将匹配全部符合条件的结果,主要用于搜索和替换。
'1a,2a,3a'.replace(/a/, 'b');
// '1b,2a,3a'
'1a,2a,3a'.replace(/a/g, 'b');
// '1b,2b,3b'
优先级
下表为正则表达式符号优先级排序,从上到下,优先级逐渐降低(优先级数值越大,优先级越高)。
| 符号 | 符号名称 | 优先级 | 
|---|---|---|
| \ | 转义符 | 5 | 
| ()(?!)(?=)[] | 括号、字符集、环视 | 4 | 
| *+?{n}{n,}{n,m} | 量词 | 3 | 
| ^$ | 起始结束位置 | 2 | 
| | | 选择 | 1 | 
由于括号的用途之一就是为量词限定作用范围,所以优先级比量词高。
/ab{2}/.test('abab');
// false
/(ab){2}/.test('abab');
// true
选择符 | 的优先级最低,比起始和结束位置都要低。
/^ab|cd$/.test('abc');
// true
/^(ab|cd)$/.test('abc');
// false
局限性
尽管 JavaScript 中的正则表达式功能比较完备,但与其他语言相比,缺少某些特性
下面列出了 JavaScript 正则表达式不支持的特性
- POSIX 字符组(只支持普通字符组和排除型字符组)
- Unicode 支持(只支持单个 Unicode 字符)
- 匹配字符串开始和结尾的 \A和\Z锚(只支持^和$)
- 逆序环视(只支持顺序环视)
- 命名分组(只支持 0-9 编号的捕获组)
- 单行模式和注释模式(只支持 m、i、g)
- 模式作用范围
- 纯文本模式