Chapter 08 / 10

JavaScript RegExp 完全指南

JS 正则的两种创建方式、所有 String/RegExp API、现代特性与 lastIndex 陷阱

两种创建方式

JavaScript 中可以用字面量语法构造函数创建正则表达式:

字面量语法(推荐)
/pattern/flags——在代码解析时编译,适合模式固定的情况。性能更好,且不需要对反斜杠进行双重转义(字面量中 \d 就是 \d,不需要写 \\d)。
构造函数语法
new RegExp(pattern, flags)——在运行时构建,适合模式需要动态生成的情况(如用户输入、变量拼接)。注意:构造函数接受字符串,需要对反斜杠双重转义(\\d 才是 \d)。
// 字面量语法(推荐,反斜杠不需要双重转义)
const re1 = /\d+/g
const re2 = /^hello\s+world$/im

// 构造函数语法(需要双重转义)
const re3 = new RegExp('\\d+', 'g')   // 等价于 /\d+/g

// 动态构建正则(必须使用构造函数)
const keyword = 'hello'
const searchRe = new RegExp(keyword, 'gi')

// ⚠️ 动态正则必须转义用户输入的特殊字符
function escapeRegex(str) {
    // 转义所有正则特殊字符
    return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
const userKeyword = escapeRegex(userInput)  // 先转义
const safeRe = new RegExp(userKeyword, 'gi')  // 再构建

String 方法:正则的主战场

JavaScript 中正则最常通过 String 的方法使用:

match() — 返回匹配信息

const str = 'test1 test2 test3'

// 无 g 标志:返回第一个匹配及详细信息
str.match(/test(\d)/)
// → ['test1', '1', index: 0, input: 'test1 test2 test3', groups: undefined]
// result[0] = 整个匹配, result[1] = 第1捕获组

// 有 g 标志:返回所有匹配的字符串数组(但失去捕获组信息!)
str.match(/test\d/g)
// → ['test1', 'test2', 'test3']

// 无匹配时返回 null(注意不是空数组)
str.match(/\d{5}/)  // → null

// 安全写法:用可选链
const result = str.match(/test(\d)/)?.[1] ?? '默认值'

matchAll() — ES2020,推荐使用

// matchAll 返回迭代器,每个元素都是完整的 Match 对象(含捕获组)
// 必须使用 g 标志
const text = '2026-01-01 和 2026-12-31'
const dateRe = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/g

for (const m of text.matchAll(dateRe)) {
    console.log(m[0])      // '2026-01-01', '2026-12-31'
    console.log(m.groups)  // { year: '2026', month: '01', day: '01' }
    console.log(m.index)   // 0, 11
}

// 转为数组
const matches = [...text.matchAll(dateRe)]
const dates = matches.map(m => m.groups)

replace() 和 replaceAll() — 替换操作

// 字符串替换(只替换第一个)
'hello world'.replace(/o/, '0')      // 'hell0 world'

// 全局替换(加 g)
'hello world'.replace(/o/g, '0')     // 'hell0 w0rld'

// 数字反向引用($1, $2...)
'2026-03-26'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$2/$3/$1')
// → '03/26/2026'

// 命名引用($<name>, ES2018)
'2026-03-26'.replace(
    /(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/,
    '$<d>/$<m>/$<y>'
)  // → '26/03/2026'

// 特殊替换符
'foo bar'.replace(/(\w+)/g, '[$&]')   // '[foo] [bar]'  $& = 整个匹配
'foo bar'.replace(/(\w+) (\w+)/, "$2 $1")  // 'bar foo'  交换

// 函数替换(动态)
'price: 100 dollars'.replace(/\d+/g, n => n * 1.1)
// → 'price: 110.00000000000001 dollars'(浮点数问题,实际使用需 toFixed)

'hello world'.replace(/\b\w/g, c => c.toUpperCase())
// → 'Hello World'(每个单词首字母大写)

// replaceAll(ES2021)不需要 g 标志的字符串替换
'a.b.c'.replaceAll('.', '-')     // 'a-b-c'(安全替换字面字符串)

search() 和 split()

// search:返回首个匹配的起始位置,无匹配返回 -1
'hello world'.search(/world/)      // 6
'hello world'.search(/xyz/)        // -1

// split:按正则分割
'a1b2c3d'.split(/\d/)              // ['a','b','c','d']
'  hello   world  '.split(/\s+/).filter(Boolean)
// → ['hello', 'world']  (filter 去除空字符串)

// split 带捕获组:分隔符也出现在结果中
'a,b,,c'.split(/(,)/)
// → ['a', ',', 'b', ',', '', ',', 'c']

RegExp 方法

test() — 测试是否匹配

// test 返回布尔值,最简单的验证方法
/^\d{4}$/.test('2026')    // true
/^\d{4}$/.test('20261')   // false
/^[a-z]+$/.test('hello')  // true

exec() — 循环提取所有匹配

// exec 带 g 标志时,每次调用返回下一个匹配
const re = /\d+/g
let m

while ((m = re.exec('a1 b22 c333')) !== null) {
    console.log(`匹配: ${m[0]},位置: ${m.index}`)
}
// 匹配: 1,位置: 1
// 匹配: 22,位置: 4
// 匹配: 333,位置: 8

// 注意:现代代码建议用 matchAll 代替 exec 循环
WARNING(lastIndex 陷阱)gy 标志的正则对象有 lastIndex 状态属性,记录下次匹配的起始位置。多次调用 exectest 时会改变这个状态。如果你在不同字符串上复用同一个带 g 标志的正则对象,lastIndex 可能导致意外行为——第一次匹配可能成功,第二次却失败(因为 lastIndex 没有被重置)。
// ⚠️ lastIndex 陷阱演示
const re = /\d+/g

re.test('hello 123')   // true,lastIndex 变为 9
re.test('hello 456')   // false!因为 lastIndex=9,超过字符串长度,重置为0
re.test('hello 789')   // true,重新从0开始

// ✅ 解决方案1:每次创建新正则
function hasDigit(str) {
    return /\d+/g.test(str)  // 字面量每次都是新对象
}

// ✅ 解决方案2:不带 g 标志(test 不需要 g)
const re2 = /\d+/  // 不加 g
re2.test('hello 123')  // true(稳定)
re2.test('hello 456')  // true(稳定)

// ✅ 解决方案3:匹配前手动重置
re.lastIndex = 0
re.test('hello 123')

ES2018+ 现代特性

命名捕获组

const { groups } = '2026-03-26'.match(
    /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
)
console.log(groups)  // { year: '2026', month: '03', day: '26' }

// 命名引用在替换中
'26 March 2026'.replace(
    /(?<day>\d+) (?<month>\w+) (?<year>\d+)/,
    '$<year>-$<month>-$<day>'
)  // → '2026-March-26'

后顾断言(Lookbehind)

// ES2018 才支持后顾断言(Python 一直支持)
'$100 €200 £300'.match(/(?<=\$)\d+/g)   // ['100']($后的数字)
'$100 €200 £300'.match(/(?<!\$)\d+/g)   // ['200', '300'](非$后的数字)

dotAll 标志(s)

/foo.bar/s.test('foo\nbar')   // true(s 让 . 匹配换行)

// 提取多行 HTML 标签内容
'<div>\n  content\n</div>'.match(/<div>(.+?)<\/div>/s)?.[1]
// → '\n  content\n'

Unicode 属性转义(\p{...},需 u 或 v 标志)

// 匹配任意语言的字母
/\p{Letter}+/u.test('Héllo')       // true(含重音字母)
/\p{Letter}+/u.test('こんにちは')    // true(日文)

// 匹配汉字
'hello 世界'.match(/\p{Script=Han}+/gu)  // → ['世界']

// 匹配表情符号
'hello 😀 world'.match(/\p{Emoji}/gu)   // → ['😀']

// 注意:一定要加 u 标志,否则 \p 无效

ES2024 新特性:v 标志与集合运算

ES2024 引入了 v 标志,对字符类进行重大扩展,支持集合差集、交集操作:

v 标志(Set Notation,ES2024)
启用字符类中的集合运算语法,支持差集(--)、交集(&&)和嵌套字符类。v 标志与 u 标志不能同时使用vu 的超集(隐含 Unicode 支持)。
// v 标志:字符集差集(Unicode 数字 减去 ASCII 数字)
const nonAsciiDigits = /[\p{Decimal_Number}--[0-9]]/v
nonAsciiDigits.test('2')   // true(全角数字)
nonAsciiDigits.test('5')    // false(ASCII 数字被排除)

// v 标志:字符集交集(ASCII 字母 和 小写字母 的交集)
const asciiLower = /[\p{ASCII}&&\p{Lowercase_Letter}]/v
asciiLower.test('a')   // true
asciiLower.test('A')   // false
asciiLower.test('ä')   // false(不是 ASCII)

// 嵌套字符类
/[[a-z][A-Z][0-9]]/v   // 等价于 [a-zA-Z0-9],但可以用集合语法组合

// 注意:v 标志中的 ] { } 必须转义(v 比 u 更严格)
// /[{]/v  → 报错,必须用 /[\{]/v 或 /[{{}]/v
WARNING(u 和 v 不能共用)v 标志是 u 的升级版,不能同时使用 /pattern/uv(会报错)。如果你需要 Unicode 属性转义 \p{...} 和集合运算,使用 v 即可(它自带 Unicode 支持)。另外,v 标志对字符类内的语法更严格:某些在 u 模式下合法的写法在 v 下会报错(如未转义的 ])。

API 对比速查表

需求PythonJavaScript
测试是否匹配bool(re.search(p,s))/p/.test(s)
获取第一个匹配re.search(p,s).group()s.match(/p/)
获取所有匹配re.findall(p,s)s.match(/p/g)
获取所有匹配+捕获组re.finditer(p,s)s.matchAll(/p/g)
替换re.sub(p, r, s)s.replace(/p/g, r)
替换(函数)re.sub(p, fn, s)s.replace(/p/g, fn)
分割re.split(p, s)s.split(/p/)
命名组访问m.group('name')m.groups.name
转义特殊字符re.escape(s)手动或用库
预编译re.compile(p)字面量(自动)

小结

本章要点