JavaScript Guidebook

JavaScript 完全知识体系

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 种情况。

字符描述
\ + 元字符匹配元字符
\ + ]\ + }右方括号和右花括号无需转义
\ + 非元字符表示一些不能打印的特殊字符
\ + 除上述其他字符默认情况匹配此字符

因为元字符有特殊的含义,所以无法直接匹配。如果要匹配它们本身,则需要在它们前面加上反斜杠 \

/1+1/.test('1+1');
// false
/1\+1/.test('1+1');
// true
/\*/.test('*');
// true
/*/.test('*');
// 报错

但实际上,并非 14 个元字符都需要转义,右方括号 ] 和右花括号 } 不需要转义

/]/.test(']');
// true
/\]/.test(']');
// true
/\}/.test('}');
// true
/}/.test('}');
// true

\ 加非元字符,表示一些不能打印的特殊字符。


字符描述
\0NUL 字符 \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');
// 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 编码的码值来确定的,码值小的在前,码值大的在后。

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(这里是没有 \ 的)匹配 xy
x{n}匹配 n 次(n 为正整数)
x{n,m}匹配至少 n 次,最多 m 次(nm 为正整数)
x{n,}匹配至少 n 次(n 为正整数)

邮政编码

// 表示邮政编码 6 位数字
const regexp = /\d{6}/;

美国英语和英国英语有些词的写法不一样,如果 travelertravellerfavorfavourcolorcolour

// 同时匹配美国英语和英国英语单词
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,}

示例:

/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.$1RegExp.$2RegExp.$3RegExp.$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() 方法中也可以引用分组,形式是 $numnum 是对应分组的编号。

2000-01-01 的形式变成 01-01-2000 的形式:

'2000-01-01'.replace(/(\d{4})-(\d{2})-(\d{2})/g, '$3-$2-$1');
//'01-01-2000'

反向引用

英文中不少单词都有重叠出现的字母,如 shootbeep。若想检查某个单词是否包含重叠出现的字母,则需要引入 反向引用(back-reference)

反向引用允许在正则表达式内部引用之前捕获分组匹配的文本,形式是 \numnum 表示所引用分组的编号。

//重复字母
/([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 才匹配

/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 编号的捕获组)
  • 单行模式和注释模式(只支持 mig
  • 模式作用范围
  • 纯文本模式

参考资料