ECMAScript正则表达式
RegExp 的创建方式、常用 API、flags、状态行为、贪婪/非贪婪、断言与常见坑。
#type / concept
#status / evergreen
#resource / javascript
#resource / ecmascript
[!info] related notes
- 所属 MOC: ecmascript-moc, ecmascript-basic-reference-types
- 相邻主题: ecmascript-type-detection, ecmascript-type-conversion
- 常见协作: ways-to-split-unicode-in-javascript
- 相关原子笔记: regex-lastindex-pitfall, regex-greedy-vs-non-greedy, regex-lookahead-lookbehind
- 语法速查: regex
- 字符串替换: javascript-string-replace-patterns
ECMAScript正则表达式
正则表达式是字符串模式匹配工具。在 ECMAScript 里,它既是一套语法,也是一种内置对象:RegExp。
最短理解
- 用来描述”什么样的文本算匹配”
- 既能做判断,也能做提取、替换、搜索、拆分
- 在 JavaScript 里要同时理解”模式语法”和”RegExp 对象状态”
两种创建方式
字面量
const reg = /abc/gi;
最常见,简洁直观。
构造函数
const reg = new RegExp("abc", "gi");
适合模式需要动态拼接的情况:
const keyword = "abc";
const reg = new RegExp(keyword);
注意转义翻倍:字符串本身就要转义一次,所以 \d 要写成 "\\d":
new RegExp("\\d+"); // 正确
new RegExp("\d+"); // 错误
核心 API
RegExp 自身
test(str)— 返回布尔值,表示是否匹配exec(str)— 返回匹配详情(含分组、索引),适合配合g循环使用
String 侧
match(reg)— 返回匹配结果,带g时返回所有匹配串(不含分组)matchAll(reg)— 返回迭代器,包含所有匹配 + 分组 + 索引(推荐用于全局匹配 + 分组)replace(reg, replacement)— 替换,支持字符串或函数作为替换值search(reg)— 返回首次匹配位置,找不到返回-1split(reg)— 按正则切割字符串
exec() 循环提取
const reg = /\d+/g;
let m;
while ((m = reg.exec("a1b22c333")) !== null) {
console.log(m[0], m.index);
}
// 1 1
// 22 3
// 333 6
matchAll() — 现代替代方案
比 exec 循环更简洁,且保留分组:
const str = "2026-03-21, 2027-04-22";
const reg = /(\d{4})-(\d{2})-(\d{2})/g;
for (const m of str.matchAll(reg)) {
console.log(m[1], m[2], m[3]);
}
// 2026 03 21
// 2027 04 22
替换函数
replace 不仅能替换成字符串,还能传函数动态生成替换内容:
"a1b2c3".replace(/\d/g, (match) => {
return String(Number(match) * 2);
});
// "a2b4c6"
结合分组:
"2026-03-21".replace(/(\d{4})-(\d{2})-(\d{2})/, (_, y, m, d) => {
return `${m}/${d}/${y}`;
});
// "03/21/2026"
Flags 详解
| Flag | 含义 | 说明 |
|---|---|---|
g | 全局匹配 | 找所有匹配项,不是只找第一个 |
i | 忽略大小写 | /abc/i 匹配 abc、ABC、AbC |
m | 多行模式 | 让 ^ 和 $ 匹配每行的开头和结尾 |
s | dotAll | 让 . 也能匹配换行符 |
u | Unicode 模式 | 按 Unicode 语义处理字符 |
y | 粘连匹配 | 从 lastIndex 位置开始必须紧接着匹配 |
s 和 u 是 ES2018+ 才稳定的,老环境注意兼容。
不带 g 和带 g 时 match() 的返回结构不同,这是常见坑:
"abc123".match(/\d+/); // ["123", index: 3, ...]
"abc123".match(/\d+/g); // ["123"]
带 g 时没有分组信息,需要分组请用 matchAll()。
最容易忽略的状态问题
带 g 或 y 的正则是有状态的,反复调用 exec() 或 test() 时会受 lastIndex 影响。
const reg = /\d/g;
console.log(reg.test("1")); // true
console.log(reg.test("1")); // false ← 反直觉
所以很多”同一个正则第一次对、第二次不对”的问题,本质上不是正则写错,而是对象状态在移动。
贪婪与非贪婪
默认量词是贪婪的,会尽可能多吃字符:
"<div>one</div><div>two</div>".match(/<div>.*<\/div>/g);
// ["<div>one</div><div>two</div>"] ← 吃太多了
在量词后加 ? 变成非贪婪:
"<div>one</div><div>two</div>".match(/<div>.*?<\/div>/g);
// ["<div>one</div>", "<div>two</div>"] ← 正确
零宽断言(Lookahead / Lookbehind)
断言匹配的是”位置条件”,不消耗字符。
(?=...)— 正向先行:后面必须跟...(?!...)— 负向先行:后面不能跟...(?<=...)— 正向后行:前面必须跟...(?<!...)— 负向后行:前面不能跟...
"12px".match(/\d+(?=px)/)[0]; // "12"(不含 px)
"$199".match(/(?<=\$)\d+/)[0]; // "199"(不含 $)
分组进阶
普通捕获组
const m = "2026-03-21".match(/(\d{4})-(\d{2})-(\d{2})/);
// m[1]="2026", m[2]="03", m[3]="21"
非捕获组 (?:...)
只想分组、不想保存结果时用,减少不必要的捕获:
/(?:ab)+/
命名分组 (?<name>...)
const reg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const m = "2026-03-21".match(reg);
m.groups.year; // "2026"
m.groups.month; // "03"
m.groups.day; // "21"
反向引用
引用前面捕获到的分组内容:
/^(\w+)\s+\1$/.test("hello hello"); // true
/^(\w+)\s+\1$/.test("hello world"); // false
匹配成对标签:
/<(\w+)>.*?<\/\1>/
// 可匹配 <div>xxx</div>、<span>yyy</span>
替换时引用分组
"2026-03-21".replace(/(\d{4})-(\d{2})-(\d{2})/, "$2/$3/$1");
// "03/21/2026"
常见坑汇总
| 坑 | 说明 |
|---|---|
| lastIndex 状态 | 带 g 的 test() / exec() 有状态,详见 regex-lastindex-pitfall |
忘加 ^ $ | /^\d{6}$/ 才是”整个字符串恰好 6 位”,/\d{6}/ 只是”包含 6 位连续数字” |
. 不匹配换行 | 跨行匹配用 /.../s 或 /[\s\S]*/ |
| 贪婪吃太多 | 详见 regex-greedy-vs-non-greedy |
| 构造函数转义翻倍 | new RegExp("\\d+") 而非 new RegExp("\d+") |
| 别用正则解析 HTML | 简单场景行,复杂嵌套结构请用 DOMParser / cheerio |
常用实例
中国手机号验证
const phoneRegex = /^1[3-9]\d{9}$/;
console.log(phoneRegex.test('13800138000')); // true
console.log(phoneRegex.test('12345678901')); // false
中国身份证号验证(18位)
const idCardRegex = /^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/;
console.log(idCardRegex.test('110101199003077758')); // true
console.log(idCardRegex.test('123456789012345678')); // false
中文字符匹配
const chineseRegex = /^[\u4e00-\u9fa5]+$/;
console.log(chineseRegex.test('你好世界')); // true
console.log(chineseRegex.test('hello你好')); // false
提取中文字符
const text = 'hello你好world世界';
const matches = text.match(/[\u4e00-\u9fa5]+/g);
console.log(matches); // ['你好', '世界']
邮政编码验证
const postalCodeRegex = /^[1-9]\d{5}$/;
console.log(postalCodeRegex.test('100000')); // true
console.log(postalCodeRegex.test('000000')); // false
QQ号验证
const qqRegex = /^[1-9][0-9]{4,}$/;
console.log(qqRegex.test('12345')); // true
console.log(qqRegex.test('01234')); // false
微信号验证
const wechatRegex = /^[a-zA-Z][-_a-zA-Z0-9]{5,19}$/;
console.log(wechatRegex.test('wechat_123')); // true
console.log(wechatRegex.test('123wechat')); // false (不能以数字开头)
车牌号验证(普通燃油车)
const plateRegex = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤川青藏琼宁][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4,5}[A-HJ-NP-Z0-9挂学警港澳]$/;
console.log(plateRegex.test('京A12345')); // true
console.log(plateRegex.test('京A1234')); // true
console.log(plateRegex.test('京A123')); // false
表单验证示例
function validateForm(data) {
const errors = [];
if (!/^1[3-9]\d{9}$/.test(data.phone)) {
errors.push('手机号格式不正确');
}
if (!/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(data.email)) {
errors.push('邮箱格式不正确');
}
if (!/^[\u4e00-\u9fa5]{2,}$/.test(data.realName)) {
errors.push('姓名需为中文且至少2个字符');
}
return errors;
}
提取价格信息
const text = '商品A价格¥199.9,商品B价格$29.99';
const prices = text.match(/[¥$]\d+(\.\d+)?/g);
console.log(prices); // ['¥199.9', '$29.99']
批量替换敏感信息
const text = '联系电话:13800138000,身份证:110101199003077758';
const masked = text
.replace(/(1[3-9]\d{9})/g, (match) => match.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'))
.replace(/([1-9]\d{5})(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]/g,
(match) => match.replace(/(\d{6})\d{8}([\dXx])/, '$1********$2'));
console.log(masked);
// 联系电话:138****8000,身份证:110101********7758
更多常用正则表达式请参考:regex
什么时候适合用正则
- 表单或文本格式校验
- 批量替换
- 从文本里抓特定模式片段
什么时候别把正则用太重
- 规则已经接近语法解析器复杂度时
- 需要处理完整结构化语言时
- 需要按用户感知字符切分 Unicode 文本时
这类情况往往更适合专门解析逻辑,而不是继续堆正则。
延伸阅读
- 看
RegExp在内置类型中的位置:ecmascript-basic-reference-types - 看字符串拆分与 Unicode 边界:ways-to-split-unicode-in-javascript
- 语法速查:regex
- 字符串替换详解:javascript-string-replace-patterns
- MDN: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Regular_expressions