Menu

JavaScript 正则表达式:test、match、replace 与捕获组

JavaScript 中正则表达式的用法:创建模式,核心方法(test、match、replace),标志,以及捕获组,配有真实示例。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

正则表达式是匹配文本的模式

正则表达式描述的是字符串的形状:“四个数字”、“一个单词后面跟一个逗号”、“任何看起来像邮箱的东西”。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 访问。

Coddy 编程语言插图

和 Coddy 一起学编程

开始