Skip to content

JavaScript 正则表达式

大白话解释: 正则表达式就像"文字的模式匹配器"。用一些特殊符号来描述你想找的文字模式,比如:

  • \d 表示数字(0-9)
  • \w 表示字母、数字、下划线
  • + 表示一个或多个
  • * 表示零个或多个

为什么要用正则?

  • 验证格式:手机号、邮箱、身份证号等格式是否正确
  • 提取信息:从字符串中提取数字、日期、URL 等
  • 替换内容:批量替换字符串中的特定模式
  • 分割字符串:按特定规则分割字符串

常见正则符号速查:

  • \d → 数字(0-9)
  • \w → 字母、数字、下划线
  • \s → 空白字符(空格、换行、制表符)
  • . → 任意字符(除换行)
  • + → 一个或多个
  • * → 零个或多个
  • ? → 零个或一个
  • {n} → 恰好 n 个
  • {n,m} → n 到 m 个
  • ^ → 开头
  • $ → 结尾
  • [] → 字符集合
  • | → 或

正则表达式(Regular Expression)用于匹配、查找、替换字符串中的模式。覆盖语法、方法、标志和常见场景。


创建方式

js
// 字面量(推荐,性能更好)
const regex1 = /abc/gi

// 构造函数(模式是动态变量时使用)
const pattern = 'abc'
const regex2 = new RegExp(pattern, 'gi')

// 从变量构建
const keyword = getUserInput()
const safe = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // 转义特殊字符
const regex3 = new RegExp(safe, 'gi')

标志(Flags)

标志含义说明
gglobal全局匹配,找到所有匹配而非第一个
iignoreCase忽略大小写
mmultiline多行模式,^ $ 匹配每一行
sdotAll. 匹配换行符
uunicode正确处理四字节 UTF-16 字符
ystickylastIndex 位置开始匹配
dindices返回匹配的开始和结束索引(ES2022)
vunicodeSets字符类集合运算与增强 Unicode 属性(ES2024)
js
/abc/gi.test('ABC')     // true
/a.c/s.test('a\nc')     // true(. 匹配 \n)
/👍/u.test('👍')         // true(Unicode 模式)

基础语法

字符匹配

js
// 普通字符
/abc/       // 匹配 "abc"

// 特殊字符(需要转义)
/\.\/\\/    // 匹配 ".\/\\"

// 转义序列
/\n/        // 换行
/\r/        // 回车
/\t/        // 制表符
/\0/        // 空字符
/\f/        // 换页
/\v/        // 垂直制表符
/\xxx/      // 八进制(如 \101 = 'A')
/\xhh/      // 十六进制(如 \x41 = 'A')
/\uhhhh/    // Unicode(如 \u4e2d = '中')

字符类

js
/[abc]/     // 匹配 a 或 b 或 c
/[^abc]/    // 匹配除 a、b、c 之外的字符
/[a-z]/     // 匹配 a 到 z
/[A-Z]/     // 匹配 A 到 Z
/[0-9]/     // 匹配 0 到 9
/[a-zA-Z]/  // 匹配所有字母
/[a-zA-Z0-9]/ // 匹配字母和数字

// 预定义字符类
/\d/        // [0-9] 数字
/\D/        // [^0-9] 非数字
/\w/        // [a-zA-Z0-9_] 单词字符
/\W/        // [^a-zA-Z0-9_] 非单词字符
/\s/        // 空白字符(空格、制表符、换行等)
/\S/        // 非空白字符
/./         // 除换行外的任意字符(加 s 标志可匹配换行)

量词

js
/a*/        // 0 次或多次
/a+/        // 1 次或多次
/a?/        // 0 次或 1 次
/a{3}/      // 恰好 3 次
/a{2,4}/    // 2 到 4 次
/a{2,}/     // 2 次或更多

// 贪婪 vs 非贪婪
/a+/        // 贪婪:尽可能多匹配
/a+?/       // 非贪婪:尽可能少匹配

// 示例
'aaa'.match(/a+/)    // ['aaa'](贪婪)
'aaa'.match(/a+?/)   // ['a'](非贪婪)

位置锚点

js
/^abc/      // 字符串开头
/abc$/      // 字符串结尾
/^abc$/     // 精确匹配整个字符串
/\bword\b/  // 单词边界
/\Bword\B/  // 非单词边界

// 示例
/^hello/.test('hello world')  // true
/world$/.test('hello world')  // true
/\bcat\b/.test('catapult')    // false
/\bcat\b/.test('the cat')     // true

分组与引用

捕获组

js
// 基本捕获组
const match = 'hello-123-world'.match(/(\w+)-(\d+)-(\w+)/)
// match[0] = 'hello-123-world'(完整匹配)
// match[1] = 'hello'(第 1 组)
// match[2] = '123'(第 2 组)
// match[3] = 'world'(第 3 组)

// 反向引用
/(a)(b)\1\2/.test('abab')  // true(\1 = 第 1 组,\2 = 第 2 组)
/(\w+)\s+\1/.test('hello hello') // true(重复单词)

命名捕获组(ES2018)

js
const match = '2024-01-15'.match(
  /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
)
console.log(match.groups.year)  // '2024'
console.log(match.groups.month) // '01'
console.log(match.groups.day)   // '15'

// 命名反向引用
/(?<word>\w+)\s+\k<word>/.test('hello hello') // true

// 在 replace 中使用命名组
'2024-01-15'.replace(
  /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/,
  '$<month>/$<day>/$<year>'
)
// '01/15/2024'

非捕获组

js
// (?:...) 不捕获,只分组
/(?:a|b)c/.test('ac') // true
'ac'.match(/(?:a|b)c/) // ['ac'](没有捕获组)

// 对比捕获组
'ac'.match(/(a|b)c/)  // ['ac', 'a'](有捕获组)

断言(Lookahead / Lookbehind)

什么是断言?

断言是一种"条件检查",它检查某个位置的前面或后面是否符合条件,但不会消耗字符。你可以把它理解成"看看隔壁,但不要走过去"。

什么时候用断言?

  1. 提取特定格式的数据:比如只提取 $ 后面的数字,不包含 $ 本身
  2. 匹配不带单位的数值:比如匹配 100 但不匹配 100px
  3. 密码强度校验:检查密码是否包含大写字母、小写字母、数字
  4. 替换时保留上下文:替换某个词但保留它前面的符号
js
// 正向前瞻:后面是 ...(但不消耗)
/\d(?=px)/.test('12px')   // true(数字后面是 px)
/\d(?=px)/.test('12em')   // false

// 负向前瞻:后面不是 ...
/\d(?!px)/.test('1px')    // false
/\d(?!px)/.test('1em')    // true

// 正向后顾:前面是 ...(ES2018)
/(?<=\$)\d+/.test('$100')  // true(前面是 $)
/(?<=\$)\d+/.test('100')   // false

// 负向后顾:前面不是 ...(ES2018)
/(?<!\$)\d+/.test('$100')  // false
/(?<!\$)\d+/.test('100')   // true

// 价格提取示例
'$100 €200 ¥300'.match(/(?<=\$)\d+/g)  // ['100']
'$100 €200 ¥300'.match(/(?<=€)\d+/g)   // ['200']
'$100 €200 ¥300'.match(/(?<=¥)\d+/g)   // ['300']

String 的正则方法

js
// 返回第一个匹配的索引,找不到返回 -1
'hello world'.search(/world/)    // 6
'hello world'.search(/xyz/)     // -1
'Hello'.search(/hello/i)        // 0

match

js
// 无 g 标志:返回第一个匹配 + 捕获组
const m = 'hello 123 world 456'.match(/(\d+)/)
// m[0] = '123', m[1] = '123', m.index = 6

// 有 g 标志:返回所有匹配(不含捕获组)
const all = 'hello 123 world 456'.match(/\d+/g)
// ['123', '456']

matchAll(ES2020)

js
// 返回所有匹配的迭代器(含捕获组 + index)
const str = 'test1test2test3'
const regex = /test(\d)/g
const matches = [...str.matchAll(regex)]

// matches[0] = ['test1', '1', index: 0]
// matches[1] = ['test2', '2', index: 5]
// matches[2] = ['test3', '3', index: 10]

replace / replaceAll

js
// replace(第一个匹配)
'hello world'.replace('world', 'JS')        // 'hello JS'
'hello world'.replace(/world/, 'JS')        // 'hello JS'

// replaceAll(所有匹配,ES2021)
'aabbcc'.replaceAll('b', 'x')               // 'aaxxcc'
'aabbcc'.replace(/b/g, 'x')                 // 'aaxxcc'

// replace + 回调函数
'hello world'.replace(/\w+/g, (match, index) => {
  return `${match}(${index})`
})
// 'hello(0) world(6)'

// replace + 命名捕获组
'2024-01-15'.replace(
  /(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/,
  '$<m>/$<d>/$<y>'
)
// '01/15/2024'

split

js
'a,b,,c'.split(',')         // ['a', 'b', '', 'c']
'a,b,,c'.split(/,+/)       // ['a', 'b', 'c'](连续逗号算一个)
'hello world  foo'.split(/\s+/) // ['hello', 'world', 'foo']

RegExp 方法

test

js
// 返回 true/false
/abc/.test('xyz abc 123')  // true
/abc/.test('xyz ABC 123')  // false
/abc/i.test('xyz ABC 123') // true

exec

js
// 返回匹配详情(含捕获组 + index),无匹配返回 null
const regex = /(\d+)-(\d+)/g
const str = 'a12-34b56-78c'

let match
while ((match = regex.exec(str)) !== null) {
  console.log(`找到: ${match[0]}, 索引: ${match.index}`)
  // 找到: 12-34, 索引: 1
  // 找到: 56-78, 索引: 7
}

常见场景

验证类

js
// 手机号(中国大陆)
/^1[3-9]\d{9}$/.test('13812345678')

// 邮箱(简化版,适合前端基本校验)
// ⚠️ 局限性:不支持带 + 的地址(如 [email protected])、不校验域名是否存在、
// 不支持国际化域名(IDN)、不匹配部分特殊字符(如 !#$%)
// 生产环境建议配合后端验证或使用专业库(如 validator.js)
/^[\w.-]+@[\w-]+(\.\w+)+$/.test('[email protected]')

// 身份证号(18 位)
/^\d{17}[\dXx]$/.test('110101199003077890')

// URL
/^https?:\/\/[\w.-]+(\/\S*)?$/.test('https://example.com/path')

// 中文字符
/^[\u4e00-\u9fa5]+$/.test('你好世界')

// 密码强度(至少 8 位,含大小写字母、数字和特殊字符)
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]).{8,}$/.test('Abc12345!')

// IP 地址(校验每段 0-255 范围)
/^((25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)$/.test('192.168.1.1')

提取类

js
// 提取所有数字
'abc123def456'.match(/\d+/g) // ['123', '456']

// 提取标签内容
'<div>hello</div><p>world</p>'.match(/<\w+>(.*?)<\/\w+>/g)
// ['<div>hello</div>', '<p>world</p>']

// 提取 URL 参数
const url = 'https://example.com?name=张三&age=25'
const params = Object.fromEntries(
  [...url.matchAll(/(\w+)=([^&]+)/g)].map((m) => [m[1], m[2]])
)
// { name: '张三', age: '25' }

// 提取文件扩展名
'image.jpg'.match(/\.(\w+)$/)?.[1] // 'jpg'

// 提取日期
'2024-01-15'.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/)
// groups: { year: '2024', month: '01', day: '15' }

替换类

js
// 千分位格式化
// \B:非单词边界(避免在开头插入逗号)
// (?=(\d{3})+(?!\d)):正向前瞻,匹配"后面紧跟 3 的倍数个数字且不再跟数字"的位置
'1234567890'.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
// '1,234,567,890'

// 驼峰转换
'hello-world-foo'.replace(/-(\w)/g, (_, letter) => letter.toUpperCase())
// 'helloWorldFoo'

// 短横线转换
'helloWorld'.replace(/([A-Z])/g, '-$1').toLowerCase()
// 'hello-world'

// 去除多余空格
'  hello   world  '.replace(/\s+/g, ' ').trim()
// 'hello world'

// HTML 转义
function escapeHtml(str) {
  return str.replace(/[&<>"']/g, (char) => ({
    '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
  })[char])
}

// 模板替换
'Hello, {{name}}! You are {{age}} years old.'.replace(
  /\{\{(\w+)\}\}/g,
  (_, key) => data[key] ?? ''
)

清理类

js
// 去除 HTML 标签
'<p>hello <b>world</b></p>'.replace(/<[^>]+>/g, '')
// 'hello world'

// 去除首尾空白
str.replace(/^\s+|\s+$/g, '')
// 等价于 str.trim()

// 去除重复字符
'aabbcc'.replace(/(.)\1+/g, '$1')
// 'abc'

// 手机号脱敏
'13812345678'.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
// '138****5678'

贪婪 vs 非贪婪

js
// 贪婪(默认):尽可能多匹配
'<div>hello</div><div>world</div>'.match(/<div>.*<\/div>/)
// ['<div>hello</div><div>world</div>'](匹配了整个字符串)

// 非贪婪(加 ?):尽可能少匹配
'<div>hello</div><div>world</div>'.match(/<div>.*?<\/div>/)
// ['<div>hello</div>'](只匹配第一个)

// 量词的贪婪与非贪婪
/a*/    vs  /a*?/     // 尽可能多  vs  尽可能少
/a+/    vs  /a+?/
/a?/    vs  /a??/
/a{2,4}/ vs /a{2,4}?/

性能优化

js
// 1. 预编译正则(避免重复创建)
const EMAIL_REGEX = /^[\w.-]+@[\w-]+(\.\w+)+$/
function isValidEmail(email) {
  return EMAIL_REGEX.test(email)
}

// 2. 避免回溯灾难
// ❌ 嵌套量词:指数级回溯
// /^(a+)+$/.test('aaaaaaaaaaaaaaaaab')

// ✅ 简化模式
/^a+$/.test('aaaaaaaaaaaaaaaaab')

// 3. 使用非捕获组(不需要引用时)
/(?:a|b)c/  // 比 /(a|b)c/ 稍快

// 4. 尽早失败
/^https?:\/\//.test(url) // 先检查协议,快速排除

// 5. 避免在循环中创建正则
// ❌
for (const str of arr) {
  if (/pattern/.test(str)) { ... }
}
// ✅
const regex = /pattern/
for (const str of arr) {
  if (regex.test(str)) { ... }
}

常见陷阱

js
// 1. g 标志的 lastIndex 问题
const regex = /a/g
regex.test('a') // true(lastIndex = 1)
regex.test('a') // false(lastIndex = 1,从位置 1 开始找,找不到)
regex.lastIndex = 0 // 手动重置

// 2. 正则中的特殊字符需要转义
// . * + ? ^ $ { } ( ) [ ] | \
'hello.world'.replace(/\./g, '-') // 'hello-world'

// 3. 转义用户输入
function escapeRegex(str) {
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}

// 4. ^ 和 $ 在多行模式下的行为
/^a/m.test('b\na')  // true(m 标志:^ 匹配每行开头)
/^a/.test('b\na')   // false(无 m 标志:^ 只匹配字符串开头)

// 5. . 不匹配换行符(除非加 s 标志)
/a.b/.test('a\nb')   // false
/a.b/s.test('a\nb')  // true

参考

个人学习笔记,部分内容借助 AI 辅助整理,仅供查阅参考,请以官方文档为准