元字符(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 种情况。
字符 | 描述 |
---|---|
\ + 元字符 | 匹配元字符 |
\ + ] 或 \ + } | 右方括号和右花括号无需转义 |
\ + 非元字符 | 表示一些不能打印的特殊字符 |
\ + 除上述其他字符 | 默认情况匹配此字符 |
因为元字符有特殊的含义,所以无法直接匹配。如果要匹配它们本身,则需要在它们前面加上反斜杠 \
。
/1+1/.test('1+1');// false/1\+1/.test('1+1');// true/\*/.test('*');// true/*/.test('*');// 报错
但实际上,并非 14 个元字符都需要转义,右方括号 ]
和右花括号 }
不需要转义
/]/.test(']');// true/\]/.test(']');// true/\}/.test('}');// true/}/.test('}');// true
\
加非元字符,表示一些不能打印的特殊字符。
字符 | 描述 |
---|---|
\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] | 一个反义或补充字符集,也叫反义字符组。匹配任意不包括括号内的字符。可以使用 - 指定一个范围。 |
// 匹配 0-9 这 10 个数字之一const regexp = /[0123456789]/;regexp.test('1');// trueregexp.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');// trueconst 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');// trueconst 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
正则表达式提供了量词,用来设定某个模式出现的次数。
邮政编码
// 表示邮政编码 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 之间。
[01]?\d\d?
2[0-4]\d
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');// trueipRegExp.test('1.1.1');// falseipRegExp.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)?
13\d|14[579]|15[0-35-9]|17[0135-8]|18\d
\d{8}
const phone = /(0|\+86)?(13\d|14[579]|15[0-35-9]|17[0135-8]|18\d)\d{8}/;phone.test('13453250661');// truephone.test('1913250661');// falsephone.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,} |
示例:
/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');// trueemail.test('q@qq');// falseemail.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');// trueconsole.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');// trueconsole.log(RegExp.$1);// 'abc'/(?:abc){2}/.test('abcabc');// trueconsole.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 才匹配 |
/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 正则表达式不支持的特性
\A
和 \Z
锚(只支持 ^
和 $
)m
、i
、g
)