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)
| 标志 | 含义 | 说明 |
|---|---|---|
g | global | 全局匹配,找到所有匹配而非第一个 |
i | ignoreCase | 忽略大小写 |
m | multiline | 多行模式,^ $ 匹配每一行 |
s | dotAll | . 匹配换行符 |
u | unicode | 正确处理四字节 UTF-16 字符 |
y | sticky | 从 lastIndex 位置开始匹配 |
d | indices | 返回匹配的开始和结束索引(ES2022) |
v | unicodeSets | 字符类集合运算与增强 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)
什么是断言?
断言是一种"条件检查",它检查某个位置的前面或后面是否符合条件,但不会消耗字符。你可以把它理解成"看看隔壁,但不要走过去"。
什么时候用断言?
- 提取特定格式的数据:比如只提取
$后面的数字,不包含$本身 - 匹配不带单位的数值:比如匹配
100但不匹配100px - 密码强度校验:检查密码是否包含大写字母、小写字母、数字
- 替换时保留上下文:替换某个词但保留它前面的符号
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 的正则方法
search
js
// 返回第一个匹配的索引,找不到返回 -1
'hello world'.search(/world/) // 6
'hello world'.search(/xyz/) // -1
'Hello'.search(/hello/i) // 0match
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') // trueexec
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) => ({
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
})[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