正则表达式是匹配文本的模式
正则表达式描述的是字符串的形状:“四个数字”、“一个单词后面跟一个逗号”、“任何看起来像邮箱的东西”。JavaScript 提供了两种创建方式:
字面量形式(模式两边是斜杠,标志放在结束斜杠之后)是你最常用的写法。当模式本身是动态的,比如要根据用户输入或变量构造时,再用 new RegExp(...)。
末尾的 i 是一个标志,表示不区分大小写。稍后会详细介绍标志。
test:是否匹配?
对正则表达式最快的提问是“这个字符串里有没有匹配?”,这就是 test:
\d 表示“任意数字”。test 只返回 true 或 false。如果你只需要一个“是或否”的答案,比如验证表单字段或过滤数组,test 就是合适的工具。
match:取出匹配到的文本
想要匹配到的文本本身时,使用字符串的 match 方法:
没有 g 标志时,match 返回一个数组,包含第一个匹配以及一些元数据(index、input)。有 g 时,它返回由所有匹配组成的普通字符串数组。如果什么都没匹配到,你得到的是 null,而不是空数组,所以要做好判断:
标志改变模式的行为
标志写在结束斜杠之后,用来调整匹配方式。最常用的有:
g:全局匹配,找出所有匹配,而不只是第一个。i:不区分大小写。m:多行模式,让^和$匹配每一行的开头和结尾,而不只是整个字符串的开头和结尾。s:dotall 模式,让.也能匹配换行符。u:支持 Unicode,许多 emoji 和非 ASCII 模式都需要它。
没有 m 时,^ 只锚定字符串的最开头。有了 m,它会锚定每一行的开头,所以 Roses 和 Violets 都能被匹配到。
字符类与量词
大多数模式的基本组成部分:
\d数字,\w单词字符(字母、数字、下划线),\s空白字符。[abc]是 a、b、c 中的一个。[^abc]是除它们之外的任意字符。[a-z]是一个范围。.是除换行符外的任意字符。*零个或多个,+一个或多个,?零个或一个。{3}恰好三个,{2,5}二到五个,{2,}两个或更多。^开头,$结尾。
组合起来:
\b 是单词边界,也就是单词字符与非单词字符之间那条看不见的界线。想要匹配“完整单词”时很有用。在代码中使用某个模式之前,想看看它能匹配什么,可以在正则表达式测试工具里试一试。
捕获组:记住匹配的一部分
圆括号会创建一个组,捕获它匹配到的内容。exec 和 match 会把这些捕获结果和整体匹配一起返回:
索引 0 是完整匹配,之后每个组各占一个位置。组一旦超过两个,按位置去数就很别扭了,所以给它们起名字:
命名组在调用处更易读,模式调整顺序后也不会出错。
replace:改写匹配到的文本
replace 接收一个模式和一个替换值。替换值可以是字符串,也可以是函数:
没有 g 时,只有第一个匹配会被替换。常见的错误是忘了这个标志,然后奇怪为什么第二个邮箱看起来还是不对。
替换字符串支持反向引用。$1、$2 等指向捕获组;$<name> 指向命名组:
如果不只是简单的替换,就传一个函数。它会以参数形式接收匹配结果和捕获组:
下划线是完整匹配(我们用不上);n 是第一个捕获组。“正则加替换函数”这种写法能应付现实中大部分的文本处理。
matchAll:带分组的每一个匹配
String.prototype.matchAll 返回一个迭代器,包含每一个匹配及其捕获组,这是带 g 标志的普通 match 做不到的:
matchAll 必须带 g 标志,否则会得到 TypeError。如果你需要随机访问而不是迭代,把它展开成数组([...text.matchAll(email)])。
转义特殊字符
. * + ? ( ) [ ] { } | \ ^ $ 这些字符在正则中有特殊含义。要按字面匹配它们,用反斜杠转义:
未转义的版本会匹配 examplexcom,因为 . 表示“任意字符”。这类 bug 很常见,而且悄无声息。如果一个正则匹配得太多,先找找有没有未转义的 .。
用用户输入构造模式时,必须先转义,否则用户可以注入正则语法:
替换字符串中的 $& 是“整个匹配”的简写。
先行断言与后行断言
有时你只想在文本后面(或前面)跟着某些内容时才匹配它,但又不想把那些内容包含在匹配结果中。这就是环视(lookaround)的作用:
(?= ...)正向先行断言:“后面跟着”。(?<= ...)正向后行断言:“前面是”。(?! ...)和(?<! ...)是对应的否定形式。
环视不会消耗字符,所以你“看”的那部分仍然可以留给模式的下一部分使用。
关于邮箱验证
你会经常碰到这个需求:“给我一个验证邮箱的正则。”老实说:别这么做。真正的邮箱语法非常复杂,任何短到能读懂的正则都会在某个方向上出错。对于表单验证,一个务实的模式就够了:
可以这样读:“一些既不是空白也不是 @ 的字符,一个 @,再来一些同样的字符,一个点,再来一些同样的字符。”它能抓住明显的拼写错误,但不假装执行 RFC 5322。要真正验证邮箱,就发一封确认邮件。
常见陷阱
几个值得牢记的坑:
- 在
replace或matchAll中忘了g。 结果是只处理第一个匹配,或者得到TypeError。 - 全局正则上有状态的
lastIndex。 带g或y标志的正则会在多次test/exec调用之间记住上次停在哪里。不要在互不相关的字符串之间复用同一个正则,要么新建一个,要么用matchAll。 - 动态模式中未转义的点和斜杠。 把用户输入塞进
new RegExp(...)之前,一定要先转义。 - 灾难性回溯。 像
(a+)+这样的嵌套量词,遇到特殊构造的输入可能会让标签页卡死。如果一个正则感觉很慢,就简化它。
下一篇:日期与时间
正则表达式处理的是文本的形状;而真实数据还会带有时间戳,需要解析、格式化和计算。下一页介绍 Date、Intl.DateTimeFormat,以及能让你远离时区 bug 的思维模型。
常见问题
JavaScript 中怎样创建正则表达式?
有两种方式。字面量形式用斜杠:/hello/i。构造函数接收一个字符串:new RegExp('hello', 'i')。模式固定时用字面量;需要在运行时根据变量构造模式时用构造函数。
JavaScript 正则中的 test、match 和 exec 有什么区别?
regex.test(str) 返回一个布尔值,只关心是否存在匹配时它最快。str.match(regex) 返回一个匹配结果数组(或 null)。regex.exec(str) 每次返回一个带捕获组的匹配,配合 g 标志时,它会通过 lastIndex 在多次调用之间记录位置。
JavaScript 中怎样用正则替换所有匹配项?
使用 g 标志:str.replace(/foo/g, 'bar')。没有 g 时,只替换第一个匹配。你也可以调用 str.replaceAll(/foo/g, 'bar'),但用正则作参数时,replaceAll 必须带上 g 标志,否则会抛出错误。
JavaScript 正则中的捕获组是什么?
模式中的圆括号会创建捕获组,记住它们匹配到的内容。/(\d{4})-(\d{2})/.exec('2024-11') 返回一个数组,索引 1 是 '2024',索引 2 是 '11'。用 (?<year>\d{4}) 给它们命名,再通过 match.groups.year 访问。