前端正则表达式完全指南:从手写不出到随手就来

写了六年前端,正则表达式用了六年------但每次写复杂一点的正则,还是得先打开搜索引擎。

/^(?:(?:\+|00)86)?1[3-9]\d{9}$/ 这是手机号校验,看得懂但默写不出来。
str.matchstr.matchAll 有什么区别?exec 为什么要放在循环里?/g 加不加到底影响什么?

这篇文章不讲形式语言理论,就一个目标:让你看完以后,正则能手写、能调试、能说清楚。


一、正则表达式到底是什么

一句话定义: 正则表达式(Regular Expression,简称 regex 或 regexp)是一套用来描述字符串匹配模式的微型语言。

你可以把它理解成一个"字符串筛子"------你定义一个模式,然后让字符串从筛子里过,符合模式的留下,不符合的滤掉。

前端工程师每天都在用正则,只是你可能没意识到:

场景 你在干什么
表单校验手机号、邮箱、身份证 用正则判断输入是否合法
str.replace(/\s+/g, '-') 用正则做字符串替换
Webpack/Vite 配置 test: /\.tsx?$/ 用正则匹配文件后缀
ESLint 规则配置 用正则匹配代码模式
路由匹配 /user/:id(\d+) 用正则约束参数格式
从接口返回的 HTML 里提取内容 用正则做文本提取

正则不是什么高深的东西,它就是前端的日常工具 。但很多人用了好几年还是"面向搜索引擎写正则",根本原因是------基础语法没有系统地过一遍

今天我们就从零开始,一块一块拼完整。


二、正则的两种创建方式

在 JavaScript 里,创建正则有两种方式:

1. 字面量写法(推荐日常使用)

javascript 复制代码
const reg = /abc/g;

特点:写起来简洁,编译时就确定了模式,不能动态拼接变量。

2. 构造函数写法(需要动态拼接时用)

javascript 复制代码
const keyword = '前端';
const reg = new RegExp(keyword, 'g');

特点:可以用变量拼接模式,运行时才编译

什么时候该用哪种?

场景 选择 原因
校验手机号、邮箱等固定模式 字面量 /^\d{11}$/ 模式固定,字面量更简洁
根据用户输入的关键词做高亮 构造函数 new RegExp(keyword, 'gi') 关键词是动态的
Webpack/Vite 配置文件里匹配文件 字面量 /\.css$/ 模式固定

踩坑提醒:构造函数里反斜杠要双写

javascript 复制代码
// 你想匹配数字 \d
const reg1 = /\d+/;           // ✅ 字面量,正常写
const reg2 = new RegExp('\\d+'); // ✅ 构造函数,反斜杠要转义

const reg3 = new RegExp('\d+'); // ❌ \d 在字符串里不是转义字符,
                                //    实际变成了 d+,匹配的是字母 d

这是很多人踩过的坑------用 new RegExp 时,字符串本身会先做一层转义,所以 \d 必须写成 \\d


三、基础语法:一块一块拼出来

正则语法看起来像乱码,但它其实就几个模块拼起来的。我们一个一个来。

3.1 普通字符------就是它自己

javascript 复制代码
/abc/.test('xabcy'); // true ------ 字符串里包含连续的 abc
/abc/.test('a-b-c'); // false ------ abc 必须连续

字母、数字、汉字这些"普通字符"在正则里就代表它自己,没有特殊含义。

3.2 特殊字符(元字符)------正则的核心能力

这些字符有特殊含义,是正则的灵魂:

元字符 含义 示例 匹配
. 匹配任意一个字符(换行符除外) /a.c/ abc、a1c、a c
\d 匹配一个数字 (等价于 [0-9] /\d{3}/ 123、456
\D 匹配一个非数字 /\D/ a、!、空格
\w 匹配一个单词字符(字母、数字、下划线) /\w+/ hello、test_1
\W 匹配一个非单词字符 /\W/ @、#、空格
\s 匹配一个空白字符(空格、Tab、换行等) /\s/ 空格、\t、\n
\S 匹配一个非空白字符 /\S+/ hello、123
\b 单词边界 /\bcat\b/ 匹配 "the cat sat" 中的 cat,不匹配 category
\B 非单词边界 /\Bcat/ 匹配 category 中的 cat

记忆技巧: 小写是"某类字符",大写是"取反"。\d = digit,\D = 非 digit;\w = word,\W = 非 word;\s = space,\S = 非 space。

3.3 量词------控制"出现几次"

量词 含义 示例 匹配
* 0 次或多次 /ab*/ a、ab、abbb
+ 1 次或多次 /ab+/ ab、abbb(不匹配单独的 a)
? 0 次或 1 次 /colou?r/ color、colour
{n} 恰好 n 次 /\d{4}/ 2025
{n,} 至少 n 次 /\d{2,}/ 12、123、1234
{n,m} n 到 m 次 /\d{2,4}/ 12、123、1234

案例:匹配手机号

javascript 复制代码
const phoneReg = /^1[3-9]\d{9}$/;

phoneReg.test('13812345678'); // true
phoneReg.test('12345678901'); // false ------ 第二位不能是2
phoneReg.test('1381234567');  // false ------ 不够11位

拆解:^ 开头 + 1 第一位是1 + [3-9] 第二位是3到9 + \d{9} 后面恰好9个数字 + $ 结尾。

3.4 贪婪与懒惰------量词的"性格"

这是一个非常容易踩坑的点。

默认情况下,量词是"贪婪"的------尽可能多匹配。

javascript 复制代码
const str = '<div>hello</div><div>world</div>';

// 贪婪模式:.* 会尽可能多吃字符
str.match(/<div>.*<\/div>/);
// 结果:['<div>hello</div><div>world</div>']
// 它一口气吃到了最后一个 </div>

// 懒惰模式:.*? 尽可能少吃字符
str.match(/<div>.*?<\/div>/);
// 结果:['<div>hello</div>']
// 它吃到第一个 </div> 就停了

规则很简单:量词后面加一个 ?,就从贪婪变成懒惰。

贪婪 懒惰 区别
* *? 尽可能多 → 尽可能少
+ +? 尽可能多 → 尽可能少
{n,m} {n,m}? 尽可能多 → 尽可能少

实战踩坑: 很多人在用正则提取 HTML 标签内容时被贪婪模式坑过。记住:提取"一对标签之间的内容"时,中间的匹配部分通常用 .*?(懒惰模式)而不是 .*(贪婪模式)。

3.5 字符集(字符类)------"这些字符里任选一个"

用方括号 [] 定义一个字符集,表示方括号里的字符任选一个

javascript 复制代码
/[abc]/.test('apple');  // true ------ 包含 a
/[abc]/.test('dog');    // false ------ 不包含 a/b/c
/[0-9]/.test('hello3'); // true ------ 包含数字
/[a-zA-Z]/.test('你好'); // false ------ 不包含英文字母

字符集里的特殊写法:

写法 含义
[abc] a 或 b 或 c
[a-z] a 到 z 的任意小写字母
[A-Z] 任意大写字母
[0-9] 任意数字(等价于 \d
[a-zA-Z0-9_] 字母+数字+下划线(等价于 \w
[^abc] 取反:除了 a/b/c 以外的任意字符
[^0-9] 非数字(等价于 \D

注意: 方括号里的 ^ 是取反的意思,别和正则开头的 ^(锚点)搞混了。

3.6 锚点------"在哪个位置匹配"

锚点不匹配字符,而是匹配位置

锚点 含义 示例
^ 字符串的开头 /^hello/ ------ 以 hello 开头
$ 字符串的结尾 /world$/ ------ 以 world 结尾
\b 单词边界 /\bJS\b/ ------ 匹配独立的 JS,不匹配 JSON 里的 JS

案例:为什么表单校验一定要加 ^$

javascript 复制代码
// 不加 ^ 和 $
/\d{11}/.test('abc13812345678xyz'); // true 😱

// 加了 ^ 和 $
/^\d{11}$/.test('abc13812345678xyz'); // false ✅
/^\d{11}$/.test('13812345678');       // true ✅

不加锚点,正则只是检查字符串里"是否包含"这个模式,而不是"整体是否匹配"。表单校验不加 ^$ 是经典新手坑。

3.7 分组与捕获------用小括号 () 打包

小括号有两个核心功能:分组捕获

功能一:分组------把多个字符当成一个整体

javascript 复制代码
// 不分组
/ab+/.test('abbb'); // true ------ + 只作用于 b

// 分组
/(ab)+/.test('ababab'); // true ------ + 作用于整个 ab

功能二:捕获------提取匹配的子串

javascript 复制代码
const dateStr = '2025-02-22';
const match = dateStr.match(/(\d{4})-(\d{2})-(\d{2})/);

console.log(match[0]); // '2025-02-22' ------ 完整匹配
console.log(match[1]); // '2025'       ------ 第1个括号捕获的
console.log(match[2]); // '02'         ------ 第2个括号捕获的
console.log(match[3]); // '22'         ------ 第3个括号捕获的

功能三:命名捕获(ES2018)------让代码更可读

javascript 复制代码
const dateStr = '2025-02-22';
const match = dateStr.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);

console.log(match.groups.year);  // '2025'
console.log(match.groups.month); // '02'
console.log(match.groups.day);   // '22'

语法:(?<名字>模式),用 match.groups.名字 获取。比数字索引可读性好太多。

功能四:非捕获分组------只分组不捕获

有时候你只是想分组,但不需要捕获结果(节省内存、让捕获组编号更清晰):

javascript 复制代码
// (?:...) 非捕获分组
const match = 'abc123'.match(/(?:abc)(\d+)/);
console.log(match[1]); // '123' ------ 第1个捕获组是 \d+,不是 abc

3.8 或运算------用 | 表示"或者"

javascript 复制代码
/cat|dog/.test('I have a cat'); // true
/cat|dog/.test('I have a dog'); // true
/cat|dog/.test('I have a fish'); // false

常见用法:配合分组限制"或"的范围。

javascript 复制代码
// 不分组:匹配 "cat" 或 "dog"
/cat|dog/

// 分组:匹配 "gray" 或 "grey"
/gr(a|e)y/

// 匹配图片后缀
/\.(png|jpe?g|gif|webp|svg)$/i

3.9 反向引用------引用前面捕获的内容

javascript 复制代码
// \1 引用第1个捕获组匹配到的内容
const reg = /(\w+)\s+\1/;

reg.test('hello hello'); // true  ------ \1 引用了 hello
reg.test('hello world'); // false ------ world ≠ hello

实用场景:检测连续重复的单词。


四、修饰符(Flags)------正则的"全局配置"

修饰符写在正则的末尾,控制匹配行为。

修饰符 名称 含义
g global 全局匹配,找到所有匹配项(不只是第一个)
i ignoreCase 忽略大小写
m multiline 多行模式,^$ 匹配每一行的开头结尾
s dotAll . 也能匹配换行符(ES2018)
u unicode 启用 Unicode 匹配(ES6)
y sticky 粘性匹配,从 lastIndex 位置开始

最常用的三个:g、i、m

javascript 复制代码
// g:全局匹配
'aaa'.match(/a/);   // ['a']       ------ 只找第一个
'aaa'.match(/a/g);  // ['a','a','a'] ------ 找所有

// i:忽略大小写
/hello/i.test('Hello'); // true

// m:多行模式
const text = 'line1\nline2\nline3';
text.match(/^\w+/g);   // ['line1']              ------ 只匹配整个字符串的开头
text.match(/^\w+/gm);  // ['line1','line2','line3'] ------ 每行开头都匹配

s 修饰符------让 . 匹配换行符

javascript 复制代码
const html = '<div>\nhello\n</div>';

// 默认:. 不匹配换行符
html.match(/<div>(.+)<\/div>/);   // null

// 加 s:. 也匹配换行符
html.match(/<div>(.+)<\/div>/s);  // 匹配成功,捕获 '\nhello\n'

u 修饰符------正确处理 Unicode

javascript 复制代码
// 不加 u:emoji 等 4 字节字符会出问题
'😀'.match(/^.$/);   // null 😱 ------ . 把 emoji 当成两个字符了
'😀'.match(/^.$/u);  // ['😀'] ✅

// 不加 u:Unicode 范围匹配不生效
/\u{61}/u.test('a'); // true

建议: 只要你的正则可能接触非 ASCII 字符(中文、emoji 等),就加上 u


五、JavaScript 中正则相关的 API------到底该用哪个

这是很多人搞混的地方。JS 里正则相关的方法分布在 RegExp 原型String 原型上,功能有重叠,选错了就踩坑。

5.1 全家福一览

方法 属于 返回值 常见用途
reg.test(str) RegExp true/false 判断是否匹配
reg.exec(str) RegExp 匹配数组 或 null 逐个提取匹配(配合循环)
str.match(reg) String 匹配数组 或 null 一次性获取匹配
str.matchAll(reg) String 迭代器 获取所有匹配及其捕获组(ES2020)
str.search(reg) String 索引 或 -1 查找第一个匹配的位置
str.replace(reg, replacement) String 新字符串 替换匹配的内容
str.replaceAll(reg, replacement) String 新字符串 替换所有匹配(ES2021)
str.split(reg) String 数组 按匹配模式分割字符串

5.2 逐个说清楚

test ------ 最简单,只问"有没有"
javascript 复制代码
/\d/.test('hello123'); // true
/\d/.test('hello');    // false

踩坑:带 g 的正则用 test 会有状态问题!

javascript 复制代码
const reg = /a/g;

reg.test('aaa'); // true(lastIndex 变成 1)
reg.test('aaa'); // true(lastIndex 变成 2)
reg.test('aaa'); // true(lastIndex 变成 3)
reg.test('aaa'); // false 😱(lastIndex 从头开始了)
reg.test('aaa'); // true(又回来了)

原因:带 g 的正则对象有一个 lastIndex 属性,每次 test/exec 调用后会更新,下次从 lastIndex 位置继续找。

解决办法:

  • 如果只是判断"有没有",不要加 g
  • 或者每次用前手动 reg.lastIndex = 0
  • 或者用字面量直接写 /a/.test(str)(每次创建新正则)
match ------ 获取匹配结果

match 的行为取决于有没有 g

javascript 复制代码
const str = 'a1b2c3';

// 不带 g:返回第一个匹配 + 捕获组信息
str.match(/([a-z])(\d)/);
// ['a1', 'a', '1', index: 0, groups: undefined]

// 带 g:返回所有匹配,但丢失捕获组信息
str.match(/([a-z])(\d)/g);
// ['a1', 'b2', 'c3'] ------ 只有完整匹配,没有捕获组了

这是 match 最大的设计问题:加了 g 就拿不到捕获组。 怎么办?用 matchAll

matchAll ------ 完美获取所有匹配 + 捕获组(推荐)
javascript 复制代码
const str = 'a1b2c3';
const matches = str.matchAll(/([a-z])(\d)/g); // 必须带 g

for (const match of matches) {
  console.log(match[0], match[1], match[2]);
}
// a1 a 1
// b2 b 2
// c3 c 3

matchAll 返回一个迭代器,每个元素都包含完整的匹配信息和捕获组。这是 ES2020 之后提取多个匹配的最佳方式。

replace ------ 替换匹配内容
javascript 复制代码
// 基本替换
'hello world'.replace(/world/, '前端');
// 'hello 前端'

// 全局替换需要加 g
'aaa'.replace(/a/, 'b');   // 'baa' ------ 只替换第一个
'aaa'.replace(/a/g, 'b');  // 'bbb' ------ 全部替换

// 用捕获组做高级替换:$1、$2 引用捕获组
'2025-02-22'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$2/$3/$1');
// '02/22/2025'

// 用函数做替换
'hello world'.replace(/\b\w/g, (char) => char.toUpperCase());
// 'Hello World'

replace 的第二个参数可以是函数,这是正则最强大的用法之一。

函数的参数:(match, p1, p2, ..., offset, string)

javascript 复制代码
// 把 HTML 中的链接提取出来做转换
const html = '访问 <a href="https://example.com">示例</a> 网站';
const result = html.replace(
  /<a href="([^"]+)">([^<]+)<\/a>/g,
  (match, url, text) => `[${text}](${url})`
);
// '访问 [示例](https://example.com) 网站'
split ------ 用正则分割字符串
javascript 复制代码
// 按逗号+可选空格分割
'a, b,c , d'.split(/,\s*/);
// ['a', 'b', 'c', 'd']

// 按多种分隔符分割
'2025-02/22'.split(/[-/]/);
// ['2025', '02', '22']

// 按一个或多个空白字符分割
'hello   world  foo'.split(/\s+/);
// ['hello', 'world', 'foo']

六、前端实战:高频正则模式

6.1 表单校验系列

javascript 复制代码
// 手机号(中国大陆)
const phoneReg = /^1[3-9]\d{9}$/;

// 邮箱(简化版,日常够用)
const emailReg = /^[\w.-]+@[\w-]+(\.[\w-]+)+$/;

// 身份证号(18位,简化校验)
const idCardReg = /^\d{17}[\dXx]$/;

// 密码强度:至少8位,包含大小写字母和数字
const pwdReg = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,}$/;

// URL
const urlReg = /^https?:\/\/[\w-]+(\.[\w-]+)+(:\d+)?(\/\S*)?$/;

关于密码正则的 (?=...) 语法: 这叫正向先行断言(Lookahead),下一节会详细讲。

6.2 字符串处理系列

javascript 复制代码
// 去除首尾空白(trim 的正则实现)
str.replace(/^\s+|\s+$/g, '');

// 把连续空白压缩成一个空格
str.replace(/\s+/g, ' ');

// 驼峰转短横线:backgroundColor → background-color
'backgroundColor'.replace(/[A-Z]/g, (char) => '-' + char.toLowerCase());

// 短横线转驼峰:background-color → backgroundColor
'background-color'.replace(/-([a-z])/g, (_, char) => char.toUpperCase());

// 千分位格式化:1234567 → 1,234,567
'1234567'.replace(/\B(?=(\d{3})+$)/g, ',');

// 提取 URL 中的查询参数
function getParams(url) {
  const params = {};
  url.replace(/[?&]([^=&#]+)=([^&#]*)/g, (_, key, value) => {
    params[decodeURIComponent(key)] = decodeURIComponent(value);
  });
  return params;
}
getParams('https://example.com?name=张三&age=25');
// { name: '张三', age: '25' }

6.3 内容提取系列

javascript 复制代码
// 提取 Markdown 中所有图片链接
const md = '![alt1](img1.png) text ![alt2](img2.jpg)';
const images = [...md.matchAll(/!\[([^\]]*)\]\(([^)]+)\)/g)].map(m => ({
  alt: m[1],
  src: m[2]
}));
// [{ alt: 'alt1', src: 'img1.png' }, { alt: 'alt2', src: 'img2.jpg' }]

// 提取所有 HTML 标签名
const html = '<div><span>text</span><img/></div>';
const tags = [...html.matchAll(/<\/?(\w+)/g)].map(m => m[1]);
// ['div', 'span', 'span', 'img', 'div']

七、进阶语法:断言(Lookaround)

断言是正则里"看起来最唬人,但用对了最强大"的语法。它不消耗字符,只做判断。

7.1 四种断言

语法 名称 含义
(?=...) 正向先行断言 后面 ...
(?!...) 负向先行断言 后面不是 ...
(?<=...) 正向后行断言 前面 ...
(?<!...) 负向后行断言 前面不是 ...

7.2 正向先行断言 (?=...)

"我要匹配一个位置,这个位置后面是 ..."

javascript 复制代码
// 匹配后面跟着 "元" 的数字
'100元200美元300元'.match(/\d+(?=元)/g);
// ['100', '300'] ------ 200 后面跟的是"美",不匹配

千分位格式化的原理就是先行断言:

javascript 复制代码
// \B      ------ 非单词边界(不在开头)
// (?=...) ------ 后面满足条件
// (\d{3})+$ ------ 后面是 3 的倍数个数字直到结尾
'1234567'.replace(/\B(?=(\d{3})+$)/g, ',');
// '1,234,567'

7.3 负向先行断言 (?!...)

"我要匹配一个位置,这个位置后面不是 ..."

javascript 复制代码
// 匹配后面不跟 "px" 的数字
'width:100px;height:200;margin:30em'.match(/\d+(?!px|em)/g);
// 注意:这个例子需要更精细的写法,但核心概念是"后面不能是 px"

// 更实际的例子:匹配不在 .min.js 中出现的 .js 文件名
const files = ['app.js', 'app.min.js', 'vendor.js', 'vendor.min.js'];
files.filter(f => /\.js$/.test(f) && !/\.min\.js$/.test(f));
// ['app.js', 'vendor.js']

7.4 后行断言 (?<=...)(?<!...)(ES2018)

javascript 复制代码
// 匹配 $ 后面的数字(价格)
'$100 ¥200 $300'.match(/(?<=\$)\d+/g);
// ['100', '300']

// 匹配不在 $ 后面的数字
'$100 ¥200 $300'.match(/(?<!\$)\d+/g);
// ['200']

7.5 密码强度校验------断言的经典应用

javascript 复制代码
// 至少8位,必须包含大小写字母和数字
const strongPwd = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;

拆解:

  • ^$:整体匹配
  • (?=.*[a-z]):先行断言------某处有小写字母
  • (?=.*[A-Z]):先行断言------某处有大写字母
  • (?=.*\d):先行断言------某处有数字
  • .{8,}:任意字符至少8个

多个先行断言可以叠加,它们都在同一个位置做检查,互不干扰。


八、真实踩坑案例

坑 1:/g 的 lastIndex 陷阱

背景: 用一个全局正则在函数里做校验。

javascript 复制代码
const emailReg = /^[\w.-]+@[\w-]+(\.[\w-]+)+$/g; // 注意这里加了 g

function isValidEmail(email) {
  return emailReg.test(email);
}

isValidEmail('test@example.com'); // true
isValidEmail('test@example.com'); // false 😱
isValidEmail('test@example.com'); // true
isValidEmail('test@example.com'); // false 😱

原因:g 的正则有 lastIndex 状态。第一次匹配成功后 lastIndex 指向字符串末尾,第二次从末尾开始找,当然找不到。

修复: 校验场景不要用 gtest 只需要知道"有没有",不需要"找所有"。

坑 2:match 加了 g 丢失捕获组

javascript 复制代码
const str = '2025-02-22, 2024-01-15';

// 想提取所有日期的年月日
const result = str.match(/(\d{4})-(\d{2})-(\d{2})/g);
// ['2025-02-22', '2024-01-15'] ------ 捕获组呢?没了!

修复:matchAll

javascript 复制代码
const matches = [...str.matchAll(/(\d{4})-(\d{2})-(\d{2})/g)];
matches.forEach(m => {
  console.log(`年:${m[1]} 月:${m[2]} 日:${m[3]}`);
});

坑 3:忘了转义特殊字符

javascript 复制代码
// 想匹配 "3.14"
/3.14/.test('3X14'); // true 😱 ------ . 是通配符,匹配了 X

// 正确写法:转义点号
/3\.14/.test('3X14'); // false ✅
/3\.14/.test('3.14'); // true ✅

需要转义的特殊字符: \ . * + ? ^ $ { } [ ] ( ) | /

当你用 new RegExp 从用户输入构造正则时,一定要先转义

javascript 复制代码
function escapeRegExp(str) {
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

const userInput = 'price: $9.99 (USD)';
const safeReg = new RegExp(escapeRegExp(userInput));
safeReg.test('price: $9.99 (USD)'); // true ✅

坑 4:用正则解析 HTML------不要这么做

javascript 复制代码
// 你可能想过这么干
const html = '<div class="box"><p>hello</p></div>';
const content = html.match(/<div[^>]*>(.*?)<\/div>/s)?.[1];

简单场景能用,但正经解析 HTML 请用 DOM API

javascript 复制代码
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const content = doc.querySelector('div.box').innerHTML;

为什么?HTML 可以嵌套、可以有注释、可以有各种边界情况,正则处理不了递归结构。

规则:正则做简单的文本匹配和提取可以;解析结构化数据(HTML、JSON),用专门的解析器。

坑 5:正则性能------回溯爆炸

javascript 复制代码
// 危险正则:可能导致"灾难性回溯"
const evilReg = /^(a+)+$/;

// 正常字符串没问题
evilReg.test('aaaaaaaaaa'); // true,很快

// 但给一个"几乎匹配"的字符串
evilReg.test('aaaaaaaaaaaaaaaaaaaaaaaab'); // false,但要等很久!

原因:(a+)+ 有多种方式分配 a 的归属,当最后无法匹配时,引擎会逐一尝试所有可能(回溯),时间复杂度指数级增长。

避免方式:

  • 避免嵌套量词:(a+)+(a*)*(a+)* 这类写法都危险
  • 可以改写为 a+,效果一样但不会回溯爆炸
  • 生产环境接受用户输入的正则时,要做超时保护

九、正则调试工具推荐

写正则不靠硬想,善用工具事半功倍:

工具 说明
regex101.com 最强正则调试工具,实时高亮、分步解释、多语言支持
regexper.com 把正则转成可视化铁路图,看结构一目了然
regexr.com 另一个在线调试器,社区正则库丰富
VSCode 搜索 Ctrl+H 开启正则模式,在编辑器里直接用正则搜索替换

强烈推荐 regex101.com------你把正则贴进去,它会逐字符给你解释这个正则在干什么,还能看到匹配的步骤。比盯着正则硬猜高效一百倍。


十、面试怎么答

面试官问"说说你对正则表达式的理解"或者"正则在前端有什么应用",可以这样组织:

第一步:简洁定义

正则表达式是一套描述字符串匹配模式的语法。在前端开发中主要用于表单校验、字符串处理、文本提取和替换。

第二步:结合实践

我在日常工作中经常用正则做表单校验(手机号、邮箱等)、用 replace 配合捕获组做格式转换(比如日期格式、驼峰命名转换)、在构建工具配置中用正则匹配文件类型。

第三步:说出一两个容易踩的坑

比如带 g 标志的正则有 lastIndex 状态,在循环校验场景下会出现时灵时不灵的 bug。再比如贪婪匹配和懒惰匹配的区别------提取标签内容时如果用了贪婪模式,会匹配到最后一个闭合标签,通常需要用 .*? 懒惰模式。

第四步(加分项):性能意识

写正则时要避免嵌套量词导致的灾难性回溯,尤其是在处理用户输入时。对于复杂的结构化数据(如 HTML),应该用 DOMParser 而不是正则。


十一、常用正则速查表

最后附一张速查表,收藏备用:

复制代码
【字符类】
.        任意字符(换行除外,加 s 修饰符则包含换行)
\d       数字 [0-9]
\D       非数字 [^0-9]
\w       单词字符 [a-zA-Z0-9_]
\W       非单词字符
\s       空白字符(空格、Tab、换行等)
\S       非空白字符
[abc]    a 或 b 或 c
[^abc]   除了 a、b、c
[a-z]    a 到 z

【量词】
*        0 次或多次
+        1 次或多次
?        0 次或 1 次
{n}      恰好 n 次
{n,}     至少 n 次
{n,m}    n 到 m 次
*?  +?   懒惰模式(尽可能少匹配)

【锚点】
^        字符串/行开头
$        字符串/行结尾
\b       单词边界
\B       非单词边界

【分组与引用】
(abc)       捕获分组
(?:abc)     非捕获分组
(?<name>)   命名捕获
\1          反向引用第 1 个捕获组
$1          replace 中引用第 1 个捕获组

【断言】
(?=...)     正向先行断言(后面是)
(?!...)     负向先行断言(后面不是)
(?<=...)    正向后行断言(前面是)
(?<!...)    负向后行断言(前面不是)

【修饰符】
g    全局匹配
i    忽略大小写
m    多行模式
s    dotAll(. 匹配换行)
u    Unicode 模式
y    粘性匹配

十二、总结

维度 说明
正则是什么 描述字符串匹配模式的微型语言
核心能力 匹配、提取、替换、分割
JS 中最常用的 API test(校验)、match/matchAll(提取)、replace(替换)
最常踩的坑 g 的 lastIndex 状态、贪婪vs懒惰、忘转义特殊字符
最佳实践 校验不加 g;提取多个用 matchAll;解析 HTML 用 DOM API
调试工具 regex101.com(强烈推荐)

最后一句话:

正则表达式就像 Git------你可以只会最基本的操作活很多年,但系统学一遍之后会发现,之前白费了好多力气。这篇文章覆盖了日常开发 95% 的正则场景,建议收藏,忘了就回来翻翻。


以上就是本次的学习分享,欢迎大家在评论区讨论指正,与大家共勉。

我是 Eugene,你的电子学友。

如果文章对你有帮助,别忘了点赞、收藏、加关注,你的认可是我持续输出的最大动力~

相关推荐
Cache技术分享1 小时前
332. Java Stream API - Java Stream 实战进阶:按年份找出合作最多的作者对
前端·后端
九狼1 小时前
Flutter Riverpod + MVI 状态管理实现的提示词优化器
前端·flutter·github
巴巴博一2 小时前
【前端架构实战】拒绝切 Tab 白屏!纯手写 Vue/uni-app 多标签页“零延迟缓存”列表架构
前端·vue.js·架构
*.✧屠苏隐遥(ノ◕ヮ◕)ノ*.✧2 小时前
Jsoup: 一款Java的HTML解析器
java·开发语言·前端·后端·缓存·html
MoSTChillax2 小时前
Figma Make:可复用 Prompt 把设计图画“准”
前端·ui·prompt·figma
m0_738120722 小时前
渗透测试——Momentum靶机渗透提取详细教程(XSS漏洞解密Cookie,SS获取信息,Redis服务利用)
前端·redis·安全·web安全·ssh·php·xss
We་ct2 小时前
LeetCode 124. 二叉树中的最大路径和:刷题解析
前端·数据结构·算法·leetcode·typescript
你怎么知道我是队长2 小时前
前端学习---VsCode相关插件安装
前端·vscode·学习
小程故事多_803 小时前
破局 LLM 黑盒困局,Phoenix 凭全链路可观测,重构大模型应用工程化落地规则
java·前端·人工智能·重构·aigc