正则表达式是什么
正则表达式(Regular Expression,简称 regex 或 regexp)是一种描述字符串模式的微型语言。它用少量元字符与量词组合出极其灵活的匹配规则,被几乎所有编程语言、文本编辑器、命令行工具支持,是开发者处理字符串的瑞士军刀。
典型应用场景:
- 数据校验:判断邮箱、手机号、URL、IP 是否符合格式
- 信息提取:从日志中抽取时间戳、IP、错误码;从 HTML 中抽取链接
- 批量替换:把 camelCase 转为 snake_case,把模板占位符替换为真实值
- 语法高亮:编辑器用正则识别关键字、字符串、注释等 token
JavaScript 内置 RegExp 类型与正则字面量语法 /pattern/flags,无需引入任何库即可使用。本文配套的在线正则测试工具可让你边读边试。
基础语法:字符类与量词
正则中的字符分为两类:字面字符(匹配自身)与元字符(有特殊含义)。需要匹配元字符本身时,用反斜杠 \ 转义。
元字符列表
| 元字符 | 含义 | 示例 |
|---|---|---|
. | 匹配除换行符外的任意字符 | a.c 匹配 abc axc a c |
* | 前一项重复 0 次或多次 | ab* 匹配 a ab abbb |
+ | 前一项重复 1 次或多次 | ab+ 匹配 ab abbb,不匹配 a |
? | 前一项出现 0 次或 1 次 | colou?r 同时匹配 color colour |
{n} {n,} {n,m} | 精确重复 n 次 / 至少 n 次 / n 到 m 次 | \d{4} 匹配 4 位数字 |
^ $ | 字符串首尾锚点(多行模式下匹配行首尾) | ^hello world$ |
\b \B | 单词边界 / 非单词边界 | \bword\b 不匹配 keyword |
| ` | ` | 或 |
[] | 字符集(匹配其中任意一个) | [aeiou] 匹配任意元音 |
[^] | 否定字符集 | [^0-9] 匹配非数字 |
() | 分组与捕获 | (ab)+ 匹配 ab abab |
预定义字符类
| 写法 | 等价于 | 含义 |
|---|---|---|
\d | [0-9] | 数字 |
\D | [^0-9] | 非数字 |
\w | [A-Za-z0-9_] | 单词字符(字母数字下划线) |
\W | [^A-Za-z0-9_] | 非单词字符 |
\s | [ \t\n\r\f\v] | 空白字符 |
\S | [^ \t\n\r\f\v] | 非空白字符 |
在 在线正则测试工具 中输入
\d+勾选g标志,粘贴一段含数字的文本,所有数字片段会被立即高亮。
标志位:g / i / m / s / u / y 的真实含义
JavaScript 正则的标志位出现在字面量末尾或 new RegExp(pattern, flags) 第二参数。理解每个标志位的真实行为是写出正确正则的前提。
| 标志 | 名称 | 行为 |
|---|---|---|
g | global | 全局匹配,找出全部匹配项;不带 g 时只匹配第一个 |
i | ignoreCase | 忽略大小写,[a-z] 也能匹配 A-Z |
m | multiline | 多行模式,^ 与 $ 匹配每行首尾而非整个字符串首尾 |
s | dotAll | . 能匹配换行符 \n(默认不能) |
u | unicode | Unicode 模式,正确处理中文、Emoji 等多字节字符 |
y | sticky | 粘滞匹配,从 lastIndex 开始且必须紧贴该位置 |
实战对比
// 不带 g:只匹配第一个
'hello world hello'.match(/hello/) // ['hello']
// 带 g:匹配全部
'hello world hello'.match(/hello/g) // ['hello', 'hello']
// 不带 i:大小写敏感
'Hello'.match(/hello/) // null
// 带 i:忽略大小写
'Hello'.match(/hello/i) // ['Hello']
// 不带 m:^ $ 匹配整个字符串首尾
'a\nb'.match(/^b/) // null
// 带 m:^ $ 匹配每行首尾
'a\nb'.match(/^b/m) // ['b']
// 不带 s:. 不匹配换行
'a\nb'.match(/a.b/) // null
// 带 s:. 匹配换行
'a\nb'.match(/a.b/s) // ['a\nb']
// 不带 u:[\u4e00-\u9fa5] 仍能匹配中文,但 . 把 Emoji 当作两个码元
'🎉'.match(/./) // ['\uD83C'](半截 surrogate pair)
// 带 u:. 正确匹配整个 Emoji
'🎉'.match(/./u) // ['🎉']
在 在线正则测试工具 中可逐个勾选标志位观察匹配变化,无需手写代码。
捕获组与非捕获组
用圆括号 () 包裹的子模式形成捕获组,按左括号出现顺序编号为 $1、$2、$3……在替换字符串与回调中可引用。
// 捕获邮箱的用户名、域名、顶级域
const re = /(\w+)@(\w+)\.(\w+)/;
const m = 'dev@example.com'.match(re);
// m[0] = 'dev@example.com'(整体匹配)
// m[1] = 'dev'($1 用户名)
// m[2] = 'example'($2 域名)
// m[3] = 'com'($3 顶级域)
// 在替换中用 $1 $2 $3 引用
'dev@example.com'.replace(re, '[$1 at $2.$3]')
// '[dev at example.com]'
非捕获组 (?:...) 仅分组不捕获,不消耗 $N 编号,性能略好:
// 用 (?:...) 仅分组,不产生 $1
'2026-07-03'.match(/(?:\d{4})-(?:\d{2})-(?:\d{2})/)
// ['2026-07-03'],无捕获组
命名捕获组 (?<name>...) 是 ES2018 引入的语法,给捕获组起名字,可读性远胜编号:
const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const m = '2026-07-03'.match(re);
// m.groups.year = '2026'
// m.groups.month = '07'
// m.groups.day = '03'
// 替换中用 ${name} 引用
'2026-07-03'.replace(re, '$<day>/$<month>/$<year>')
// '03/07/2026'
替换:String.replace 与 $ 引用
String.prototype.replace 接受正则与替换字符串,替换字符串支持以下特殊写法:
| 写法 | 含义 |
|---|---|
$1 $2 … $9 | 第 N 个捕获组 |
$<name> | 命名捕获组 |
$$ | 字面 $ |
$& | 整体匹配文本 |
$` | 匹配文本左侧的内容 |
$' | 匹配文本右侧的内容 |
// 把 kebab-case 转 camelCase
'foo-bar-baz'.replace(/-(\w)/g, (_, c) => c.toUpperCase())
// 'fooBarBaz'
// 把每个单词首字母大写
'hello world foo'.replace(/\b(\w)/g, (_, c) => c.toUpperCase())
// 'Hello World Foo'
// 用 $& 在数字前后加方括号
'a1b2c3'.replace(/\d/g, '[$&]')
// 'a[1]b[2]c[3]'
在 在线正则测试工具 切换到「替换」Tab,输入模式
(\w+)@(\w+)\.(\w+)与替换[$1 at $2.$3],可即时看到替换结果。
零宽断言:往前看与往后看
零宽断言(lookaround)匹配位置而非字符,类似 \b 但更灵活。
| 写法 | 名称 | 含义 |
|---|---|---|
(?=...) | 正向先行断言 | 右侧必须匹配 ... |
(?!...) | 负向先行断言 | 右侧必须不匹配 ... |
(?<=...) | 正向后行断言(ES2018) | 左侧必须匹配 ... |
(?<!...) | 负向后行断言(ES2018) | 左侧必须不匹配 ... |
// 匹配数字后面的单位(数字本身不消费)
'30px 20em 10rem'.match(/\d+(?=px|em|rem)/g)
// ['30', '20', '10']
// 匹配不在 $ 后的数字
'price: $100, count: 50'.match(/(?<!\$)\d+/g)
// ['50']($100 被排除)
// 匹配 Markdown 链接文字
'[Google](https://google.com)'.match(/(?<=\[)[^\]]+(?=\])/)
// ['Google']
常用正则速查
以下模式收录在 在线正则测试工具 的「常用」速查栏,可一键载入:
// 邮箱(简化版,RFC 5322 完整版极长不实用)
/[\w.+-]+@[\w-]+\.[\w.-]+/g
// URL(http/https)
/https?:\/\/[\w.-]+(?:\/[\w./-]*)?/g
// IPv4
/\b(?:\d{1,3}\.){3}\d{1,3}\b/g
// 中国大陆手机号
/1[3-9]\d{9}/g
// 中文字符
/[\u4e00-\u9fa5]+/g
// HTML 标签
/<[^>]+>/g
// 整数(含负数)
/-?\d+/g
// 浮点数
/-?\d+\.\d+/g
上述模式适合 90% 的日常场景。生产环境校验邮箱、URL 应使用专门的库(如
validator.js),简化版可能漏判边界情况。
ReDoS:正则的暗坑
正则表达式可能遭受 ReDoS(Regular Expression Denial of Service) 攻击:恶意构造的输入让正则引擎指数级回溯,导致浏览器/服务器卡死。
危险模式
// 危险:嵌套量词 + 重叠交替
const evil = /^(a+)+$/;
evil.test('aaaaaaaaaaaaaaaaaaaaaaaaaaa!'); // 卡顿数十秒
危险信号:
- 嵌套量词:
(a+)+(a*)*(a|b)* - 重叠交替:
(a|a)*(\d|\w)+ - 大重复次数:
a{1,1000}与后续必须失败的项
防护策略
- 避免嵌套量词,用具体字符类替代
.,如[a-z][\w-] - 优先使用非捕获组
(?:...)减少状态保存 - 使用原子组或占有量词(JavaScript 暂不支持,可用
re2库替代) - 对超时敏感场景设置超时,如 Node.js 的
re2模块基于 Google RE2 引擎,线性时间保证不回溯 - 本地工具设输入长度上限,本工具对输入长度(10000 字)与匹配数(5000 个)设上限保护浏览器
安全的正则示例
// 危险:^(a+)+$
// 安全:直接用 ^a+$ 等价且不回溯
/^a+$/.test('aaaaaaaa!'); // 立即返回
// 危险:邮箱 ^(.*@.+)$
// 安全:用具体字符类
/^[\w.+-]+@[\w-]+\.[\w.-]+$/.test('dev@example.com');
实战技巧
1. 调试正则时用 source 属性查看模式
const re = /\d{4}-\d{2}-\d{2}/;
re.source; // '\\d{4}-\\d{2}-\\d{2}'
re.flags; // ''
2. 用 exec 循环遍历所有匹配(含捕获组)
const re = /(\w+)=(\d+)/g;
const text = 'a=1, b=2, c=3';
let m;
while ((m = re.exec(text)) !== null) {
console.log(`键: ${m[1]}, 值: ${m[2]}, 位置: ${m.index}`);
}
// 键: a, 值: 1, 位置: 0
// 键: b, 值: 2, 位置: 5
// 键: c, 值: 3, 位置: 10
3. 注意 lastIndex 陷阱
带 g 标志的 RegExp 对象有状态,重复使用 test() 会在匹配间游走:
const re = /\d/g;
re.test('1'); // true, lastIndex = 1
re.test('1'); // false, lastIndex = 0(找不到下一个)
re.test('1'); // true, lastIndex = 1
// 解决:用前 reset,或不用 g 标志
re.lastIndex = 0;
4. 用 String.matchAll 一次性拿到所有匹配(ES2020)
const text = 'a=1, b=2, c=3';
const matches = [...text.matchAll(/(\w+)=(\d+)/g)];
// matches[0].index = 0
// matches[0][1] = 'a'
// matches[0][2] = '1'
速记小结
g全局 /i忽略大小写 /m多行 /sdotAll /uUnicode /y粘滞(...)捕获组编号为$1 $2 ...,(?:...)非捕获组不编号- 命名捕获组
(?<name>...)在替换中用$<name>引用 - 替换字符串中
$$转义为字面$ - ReDoS 危险信号:嵌套量词
(a+)+、重叠交替(a|a)* - 生产环境校验邮箱/URL 用专门库,简化正则可能漏判边界
打开在线正则测试工具亲手试一试本文的每个示例,正则语法在「敲一遍」之后才会真正掌握。