Skip to content

正则表达式实战

正则擅长描述"形状可枚举、结构稳定"的文本模式。它的强大常常被滥用:把本该用解析器处理的格式(HTML、嵌套结构)也塞进正则,最后写出一个没人能维护的怪物。这一篇讲实战能用到的部分,以及边界在哪。

常用模式:先记住几个能覆盖大多数业务的

js
// 邮箱:够用就行,不必追求 RFC 完整
const email = /^[\w.+-]+@[\w-]+(\.[\w-]+)+$/

// 中国大陆手机号(11 位,1 开头)
const phone = /^1[3-9]\d{9}$/

// URL(简化版,只校验协议和主机)
const url = /^https?:\/\/[\w.-]+(:\d+)?(\/[^\s]*)?$/

// 18 位身份证号(含末位校验位的简化版本)
const idCard = /^\d{17}[\dXx]$/

业务校验不要追求"完美正则"。邮箱 RFC 5322 的完整正则长达几千字符,没有意义。校验到"看起来对"就够了,真正有效与否要让后端发邮件/发短信验证。

捕获组与反向引用

() 分组既能提取子串,也能配合反向引用匹配重复内容:

js
// 提取日期各部分
const m = '2026-08-09'.match(/(\d{4})-(\d{2})-(\d{2})/)
console.log(m[1], m[2], m[3]) // 2026 08 09

// 反向引用:匹配成对单词
'hello hello world'.match(/(\w+)\s\1/) // 'hello hello'

// 命名捕获组更易读
const re = /(?<year>\d{4})-(?<month>\d{2})/
const { year, month } = re.exec('2026-08').groups
写法含义
(abc)捕获组
(?:abc)非捕获组(不保存匹配,性能更好)
(?<name>abc)命名捕获组
\1反向引用第 1 个捕获组

不需要提取的分组一律用 (?:...),省内存也更快。

贪婪与非贪婪

默认量词是贪婪的,会尽可能多匹配;加 ? 变成非贪婪(懒惰),尽可能少匹配。这是处理"配对标签/引号内容"的关键。

js
const html = '<a>1</a><a>2</a>'

html.match(/<a>.*<\/a>/)  // 贪婪:'<a>1</a><a>2</a>',跨过中间的标签
html.match(/<a>.*?<\/a>/) // 非贪婪:'<a>1</a>',最短匹配

处理 HTML/JSON 这种结构化文本,正则只能做粗筛,真正的解析要交给 DOMParser 或 JSON.parse

replace 的回调:被低估的能力

replace 第二个参数可以是函数,能拿到每个捕获组、位置和原串,做复杂替换很顺手:

js
// 模板插值
'你好 {name},余额 {amount}'.replace(/\{(\w+)\}/g, (_, key) => data[key])

// 千分位
'1234567'.replace(/\B(?=(\d{3})+(?!\d))/g, ',') // '1,234,567'

// 敏感词脱敏
'18612345678'.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') // '186****5678'

test / exec / match 怎么选

方法用途注意
re.test(str)只判断是否匹配返回 boolean,最快
str.match(re)提取匹配内容g 标志返回所有匹配数组;不带返回第一组+捕获组
re.exec(str)反复匹配、提取带位置信息配合 g 可循环调用
str.replace(re, ...)替换回调形式非常强大

g 标志的 exec 会记住 lastIndex,循环调用可以遍历所有匹配;但同一个正则对象在多次 test 调用时也会受 lastIndex 影响,是个经典坑:

js
const re = /a/g
re.test('abc') // true,lastIndex 变为 1
re.test('abc') // false!lastIndex 已经超过

需要无状态判断时,要么不加 g,要么每次用新字面量。

性能与 ReDoS

正则在最坏情况下的回溯可能指数级增长。攻击者构造的特殊字符串能让一个看似正常的正则把 CPU 跑满,这就是 ReDoS(正则拒绝服务)。

危险模式:嵌套量词、量词叠加、交替分支里的重叠。例如 (a+)+(a|a)*(\w+)+ 都是高危写法。

js
// 危险:用户输入能让它卡死
/(a+)+$/.test('a'.repeat(30) + '!') // 指数级回溯

// 更安全:避免重叠量词
/a+$/.test('a'.repeat(30) + '!')

防范:对用户输入别用复杂正则;服务端校验要设超时;优先用更确定的字符类(如 [a-z] 而非 \w)配合有限重复 {1,20}

何时别用正则

  • 解析 HTML / XML / JSON:用专用解析器。
  • 嵌套层数不定的结构(括号配对、标签嵌套):正则不是上下文无关文法解析器。
  • 简单的"是否包含某子串":includes / startsWith 更快更可读。
  • 需要在多个分支里做语义判断的:写成普通函数更容易维护。

一个判断标准:如果一周后你自己都看不懂这行正则,就该拆成函数。

检查表

  • 校验类正则够用即可,不追求覆盖所有边缘 RFC 情况。
  • 不需要提取的分组用 (?:...),命名捕获组提升可读性。
  • 注意贪婪/非贪婪的区别,处理成对内容用非贪婪。
  • 警惕嵌套量词带来的 ReDoS 风险,用户输入场景务必设上限。
  • HTML、嵌套结构、JSON 解析用专用工具,别硬塞给正则。

为复用而记录,为理解而整理。