前端正则表达式完全指南:从记不住、写不出,到手写、调试、面试一把抓

前端正则表达式完全指南:从记不住、写不出,到手写、调试、面试一把抓
【正则表达式】+【前端开发】:从【核心语法】到【实战应用】,彻底搞懂【字符串匹配/校验/提取】的最佳写法,避开lastIndex/贪婪匹配/回溯爆炸高频坑!

📑 文章目录

同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~

(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)

你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?

你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?

就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。

一天只有24小时,时间永远不够用,常常感到力不从心。

技术行业,本就是逆水行舟,不进则退。

如果你也有同样的困扰,别慌。

从现在开始,跟着我一起心态归零,利用碎片时间,来一次彻彻底底的基础扫盲

这一次,我们一起慢慢来,扎扎实实变强。

不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,

咱们一起稳步积累,真正摆脱“面向搜索引擎写代码”的尴尬。

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

/^(?:(?:+|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 里,创建正则有两种方式:

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

const reg =/abc/g;

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

⬆ 返回目录

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

const keyword ='前端';const reg =newRegExp(keyword,'g');

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

⬆ 返回目录

2.3 什么时候该用哪种?

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

⬆ 返回目录

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

// 你想匹配数字 \dconst reg1 =/\d+/;// ✅ 字面量,正常写const reg2 =newRegExp('\\d+');// ✅ 构造函数,反斜杠要转义const reg3 =newRegExp('\d+');// ❌ \d 在字符串里不是转义字符,// 实际变成了 d+,匹配的是字母 d

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

⬆ 返回目录


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

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

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

/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

案例:匹配手机号

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 贪婪与懒惰——量词的"性格"

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

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

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 字符集(字符类)——“这些字符里任选一个”

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

/[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

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

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

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

⬆ 返回目录

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

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

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

// 不分组/ab+/.test('abbb');// true —— + 只作用于 b// 分组/(ab)+/.test('ababab');// true —— + 作用于整个 ab

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

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)——让代码更可读

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.名字 获取。比数字索引可读性好太多。

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

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

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

⬆ 返回目录

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

/cat|dog/.test('I have a cat');// true/cat|dog/.test('I have a dog');// true/cat|dog/.test('I have a fish');// false

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

// 不分组:匹配 "cat" 或 "dog"/cat|dog/// 分组:匹配 "gray" 或 "grey"/gr(a|e)y/// 匹配图片后缀/\.(png|jpe?g|gif|webp|svg)$/i 

⬆ 返回目录

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

// \1 引用第1个捕获组匹配到的内容const reg =/(\w+)\s+\1/; reg.test('hello hello');// true —— \1 引用了 hello reg.test('hello world');// false —— world ≠ hello

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

⬆ 返回目录


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

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

修饰符名称含义
gglobal全局匹配,找到所有匹配项(不只是第一个)
iignoreCase忽略大小写
mmultiline多行模式,^$ 匹配每一行的开头结尾
sdotAll. 也能匹配换行符(ES2018)
uunicode启用 Unicode 匹配(ES6)
ysticky粘性匹配,从 lastIndex 位置开始

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

// 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'] —— 每行开头都匹配

⬆ 返回目录

4.2 s 修饰符——让 . 匹配换行符

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

⬆ 返回目录

4.3 u 修饰符——正确处理 Unicode

// 不加 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)RegExptrue/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 —— 最简单,只问"有没有"
/\d/.test('hello123');// true/\d/.test('hello');// false

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

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

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 —— 完美获取所有匹配 + 捕获组(推荐)
const str ='a1b2c3';const matches = str.matchAll(/([a-z])(\d)/g);// 必须带 gfor(const match of matches){ console.log(match[0], match[1], match[2]);}// a1 a 1// b2 b 2// c3 c 3

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

replace —— 替换匹配内容
// 基本替换'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)

// 把 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 —— 用正则分割字符串
// 按逗号+可选空格分割'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 表单校验系列

// 手机号(中国大陆)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,}$/;// URLconst urlReg =/^https?:\/\/[\w-]+(\.[\w-]+)+(:\d+)?(\/\S*)?$/;

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

⬆ 返回目录

6.2 字符串处理系列

// 去除首尾空白(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 中的查询参数functiongetParams(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 内容提取系列

// 提取 Markdown 中所有图片链接const md ='![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.ZEEKLOGimg.cn/images/20230724024159.png?origin_url=img1.png&pos_id=img-dDYjdsPU1.png7 text !!alt2[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.ZEEKLOGimg.cn/images/20230724024159.png?origin_url=img2.jpg&pos_id=img-mtvRJKdO-1771834249689)';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']

首先要明确一个最基础的点:

正则是匹配字符串的工具(比如匹配手机号138xxxx1234、匹配邮箱[email protected]),它本来就不是用来“算数学题”的(比如判断3的倍数)。

就像你拿菜刀能勉强拧螺丝,但效率低、还容易出错——正则匹配倍数就是这个道理,能做,但不是最优解

生产环境里,我们判断一个数字是不是3的倍数,傻子都知道这么写:

// 比如判断"123"是不是3的倍数const num =123; console.log(num %3===0);// true

这行代码的意思是:123除以3,余数是不是0?是就是3的倍数。简单、好懂、不出错。

而“用正则匹配3的倍数”,本质是“绕个弯”——把“数学计算”转化成“字符串规律匹配”,纯粹是练手/面试趣味题,不是正经干活的写法。

⬆ 返回目录

6.4 趣味实战:正则匹配数字倍数

很多同学会好奇:正则能不能直接匹配 3 ⁄ 7 的倍数?答案是“可以,但分场景”

首先要明确一个最基础的点:

正则是匹配字符串的工具(比如匹配手机号138xxxx1234、匹配邮箱[email protected]),它本来就不是用来“算数学题”的(比如判断3的倍数)。

就像你拿菜刀能勉强拧螺丝,但效率低、还容易出错——正则匹配倍数就是这个道理,能做,但不是最优解

生产环境里,我们判断一个数字是不是3的倍数,傻子都知道这么写:

// 比如判断"123"是不是3的倍数const num =123; console.log(num %3===0);// true

这行代码的意思是:123除以3,余数是不是0?是就是3的倍数。简单、好懂、不出错。

而“用正则匹配3的倍数”,本质是“绕个弯”——把“数学计算”转化成“字符串规律匹配”,纯粹是练手/面试趣味题,不是正经干活的写法。

第一部分:先搞懂“3的倍数”到底有啥规律?

不用管正则,先记一个小学数学知识点:

一个数,把它的每一位数字加起来,如果和能被3整除,那这个数就是3的倍数。

比如:

  • 123:1+2+3=6 → 6能被3整除 → 123是3的倍数;
  • 4567:4+5+6+7=22 → 22÷3余1 → 不是3的倍数;
  • 0:0÷3=0 → 也是3的倍数。

这个规律是关键!正则能“匹配”3的倍数,全靠这个规律——因为正则看不懂“123”是数字,但能拆成“1”“2”“3”三个字符,然后我们用代码把这三个字符加起来,判断和是不是3的倍数。

“3的倍数”代码(逐行解释)
// 定义一个函数,输入是字符串(比如"123"),输出是true/falsefunctionisMultipleOf3(str){// 第一步:先校验输入是不是纯数字字符串(比如"123"是,"12a3"不是)// /^\d+$/ 这个正则的意思:从开头到结尾,全是数字const isPureNumber =/^\d+$/.test(str);if(!isPureNumber){returnfalse;// 不是纯数字,直接返回false}// 第二步:把字符串拆成单个字符,比如"123"拆成["1","2","3"]const digitList = str.split('');// 第三步:把每个字符转成数字,然后加起来// 初始和是0,逐个加:0+1=1 → 1+2=3 → 3+3=6let sum =0;for(let i =0; i < digitList.length; i++){ sum = sum +Number(digitList[i]);// Number()把字符"1"转成数字1}// 第四步:判断和是不是3的倍数return sum %3===0;}// 测试一下: console.log(isMultipleOf3("0"));// 0→和是0→0%3=0→true console.log(isMultipleOf3("3"));// 和是3→true console.log(isMultipleOf3("12"));// 1+2=3→true console.log(isMultipleOf3("124"));// 1+2+4=7→7%3=1→false

你看,这里虽然用了正则(/^\d+$/),但它只负责“校验是不是纯数字”,真正判断3的倍数的,还是“求和→取模”这个数学逻辑


第二部分:7的倍数为啥更麻烦?

7的倍数没有3那么简单的“求和规律”,它的规律是“递归减法”,可以记这个口诀:

把数字最后一位抠出来×2,用前面的数减去这个结果,要是差是7的倍数,那原数就是7的倍数(如果差还是大数,就重复这个操作)。

举个例子,判断“161”是不是7的倍数:

  1. 最后一位是1 → 1×2=2;
  2. 前面的数是16 → 16-2=14;
  3. 14是7的倍数 → 所以161是7的倍数。

再比如判断“1234”:

  1. 最后一位4×2=8 → 前面的数123-8=115;
  2. 115还是大数,继续:最后一位5×2=10 → 前面的数11-10=1;
  3. 1不是7的倍数 → 所以1234不是7的倍数。
“7的倍数”代码(逐行解释)
functionisMultipleOf7(str){// 第一步:先校验是不是纯数字字符串(和3的倍数一样)if(!/^\d+$/.test(str)){returnfalse;}// 第二步:把字符串赋值给临时变量,方便循环处理let tempStr = str;// 第三步:循环处理,直到数字只剩1位while(tempStr.length >1){// 抠最后一位:比如"161"→"1"const lastDigit = tempStr.slice(-1);// 抠前面的数:比如"161"→"16";如果是"7",前面没数就是"0"const restDigit = tempStr.slice(0,-1)||"0";// 计算:前面的数 - 最后一位×2(转成BigInt是为了兼容超大数)const diff = Math.abs(BigInt(restDigit)-2n*BigInt(lastDigit));// 把差值转回字符串,继续循环 tempStr = diff.toString();}// 第四步:最后只剩1位,判断是不是0或7(因为0和7都是7的倍数)// 这里用正则/^(0|7)$/:要么是0,要么是7return/^(0|7)$/.test(tempStr);}// 测试例子: console.log(isMultipleOf7("0"));// true console.log(isMultipleOf7("7"));// true console.log(isMultipleOf7("14"));// 14→最后一位4×2=8,1-8=-7→绝对值7→true console.log(isMultipleOf7("161"));// 161→16-2=14→14→1-8=-7→true console.log(isMultipleOf7("1234"));// 最后得到1→false

最后:必须记住的3个关键点(划重点)
  1. 别钻牛角尖:正则的核心作用是“匹配字符串模式”(比如校验手机号、邮箱),不是“算数学题”;判断3/7的倍数,优先用“转数字→取模”(num % 3 === 0),简单又不容易错;
  2. 3的倍数规律:各位数字相加的和能被3整除,这是最容易理解的规律,不用记复杂的正则;
  3. 7的倍数规律:最后一位×2,前面的数减这个结果,递归到只剩1位,看是不是0/7就行。

⬆ 返回目录


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

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

7.1 四种断言

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

⬆ 返回目录

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

“我要匹配一个位置,这个位置后面是 …”

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

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

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

⬆ 返回目录

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

“我要匹配一个位置,这个位置后面不是 …”

// 匹配后面不跟 "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)

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

⬆ 返回目录

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

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

拆解:

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

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

⬆ 返回目录


八、真实踩坑案例

8.1 坑 1:/g 的 lastIndex 陷阱

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

const emailReg =/^[\w.-]+@[\w-]+(\.[\w-]+)+$/g;// 注意这里加了 gfunctionisValidEmail(email){return emailReg.test(email);}isValidEmail('[email protected]');// trueisValidEmail('[email protected]');// false 😱isValidEmail('[email protected]');// trueisValidEmail('[email protected]');// false 😱

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

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

⬆ 返回目录

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

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

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

⬆ 返回目录

8.3 坑 3:忘了转义特殊字符

// 想匹配 "3.14"/3.14/.test('3X14');// true 😱 —— . 是通配符,匹配了 X// 正确写法:转义点号/3\.14/.test('3X14');// false ✅/3\.14/.test('3.14');// true ✅

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

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

functionescapeRegExp(str){return str.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');}const userInput ='price: $9.99 (USD)';const safeReg =newRegExp(escapeRegExp(userInput)); safeReg.test('price: $9.99 (USD)');// true ✅

⬆ 返回目录

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

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

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

const parser =newDOMParser();const doc = parser.parseFromString(html,'text/html');const content = doc.querySelector('div.box').innerHTML;

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

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

⬆ 返回目录

8.5 坑 5:正则性能——回溯爆炸

// 危险正则:可能导致"灾难性回溯"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 中最常用的 APItest(校验)、match/matchAll(提取)、replace(替换)
最常踩的坑g 的 lastIndex 状态、贪婪vs懒惰、忘转义特殊字符
最佳实践校验不加 g;提取多个用 matchAll;解析 HTML 用 DOM API
调试工具regex101.com(强烈推荐)

最后一句话:

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

⬆ 返回目录


学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。

后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。

关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。

如果你觉得这篇内容对你有帮助,不妨点赞+收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。

我是 Eugene,你的电子学友,我们下一篇干货见~

Read more

腾讯云智能客服机器人Java集成实战:从接入到生产环境优化

最近在项目中接入了腾讯云的智能客服机器人,把整个集成过程和一些优化经验记录下来,希望能帮到有类似需求的同学。自己动手搭过客服系统的朋友都知道,从零开始搞一套,不仅开发周期长,而且智能语义理解这块的门槛太高了,效果还很难保证。直接集成成熟的SaaS服务,就成了一个快速又靠谱的选择。 1. 为什么选择腾讯云智能客服? 在技术选型阶段,我们对比了几家主流云厂商的方案。阿里云的智能客服功能也很强大,生态完善,但它的API设计风格和我们团队的历史技术栈适配起来有点别扭。AWS Lex的优势在于和海外其他AWS服务无缝集成,但国内访问的延迟和合规性是需要考虑的问题。腾讯云智能客服吸引我们的点主要有几个: * API设计友好:它的RESTful API文档清晰,错误码规范,并且提供了Java、Python等多种语言的SDK,上手速度快。 * 计费透明灵活:支持按调用量、按坐席等多种计费模式,初期可以先用调用量模式试水,成本可控。 * NLP能力本土化强:在中文场景下的意图识别和情感分析准确率不错,特别是针对一些行业术语和网络用语,理解得比较到位。 综合来看,对于国内业务为主、追求快速集

By Ne0inhk
JAVA项目实战:用飞算 JavaAI 高效开发电商系统核心功能模块

JAVA项目实战:用飞算 JavaAI 高效开发电商系统核心功能模块

JAVA项目实战:用飞算 JavaAI 高效开发电商系统核心功能模块 * 一、前言 * 二、飞算JavaAI使用 * 三、需求分析与规划 * (一)功能需求 * (二)核心模块 * (三)技术选型 * 四、飞算JavaAI开发实录 * (一)初始化项目 * (二)商品管理模块开发 * (三)订单模块开发 * (四)用户中心模块开发 * 五、优化与调试心得 * (一)遇到的问题及解决 * (二)飞算JavaAI其他功能利用 * 六、成果展示与总结 * (一)工程结构图 * (二)核心代码片段 * (三)飞算JavaAI使用体会 * 七、总结评价 * 结束语 JAVA项目实战:用飞算 JavaAI 高效开发电商系统核心功能模块。本文围绕用飞算 JavaAI 开发电商系统核心功能模块展开。

By Ne0inhk
Java 大视界 -- Java 大数据在智能医疗临床路径优化与医疗资源合理利用中的应用(424)

Java 大视界 -- Java 大数据在智能医疗临床路径优化与医疗资源合理利用中的应用(424)

Java 大视界 -- Java 大数据在智能医疗临床路径优化与医疗资源合理利用中的应用(424) * 引言: * 正文: * 一、智能医疗临床路径与资源利用的核心痛点 * 1.1 临床路径的 “固化与滞后” 困境 * 1.1.1 路径执行的 “千人一面” * 1.1.2 指南更新的 “落地延迟” * 1.2 医疗资源的 “调度失衡” 痛点 * 1.2.1 设备资源的 “闲置与紧缺并存” * 1.2.2 医护人力的 “错配” * 1.3 医疗数据的 “孤岛与安全” 挑战 * 1.3.1 数据孤岛导致 “决策失明”

By Ne0inhk
【YF技术周报 Vol.01】OpenAI 国会指控 DeepSeek,字节发布 Seedance 2.0,Java 26 预览版来了

【YF技术周报 Vol.01】OpenAI 国会指控 DeepSeek,字节发布 Seedance 2.0,Java 26 预览版来了

🍃 予枫:个人主页 📚 个人专栏: 《Java 从入门到起飞》《读研码农的干货日常》 💻 Debug 这个世界,Return 更好的自己! 文章目录 * 🚨 1. OpenAI 向美国国会提交备忘录:指控 DeepSeek “非法蒸馏” * 🎬 2. 字节跳动发布 Seedance 2.0:对标 Sora 的视频生成模型 * 🛑 3. OpenAI 正式下线 GPT-4o,全面转向 GPT-5 * ☕ 4. Azul 发布《2026 Java 现状报告》:AI 开发中的 Java 渗透率攀升 * 💡 YF 的深度思考:护城河与工具链 👋 卷首语 大家好,我是予枫。 这是 《YF 技术周报》

By Ne0inhk