ECMAScript正则表达式

RegExp 的创建方式、常用 API、flags、状态行为、贪婪/非贪婪、断言与常见坑。

#type / concept #status / evergreen #resource / javascript #resource / ecmascript

[!info] related notes

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) — 返回首次匹配位置,找不到返回 -1
  • split(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 匹配 abcABCAbC
m多行模式^$ 匹配每行的开头和结尾
sdotAll. 也能匹配换行符
uUnicode 模式按 Unicode 语义处理字符
y粘连匹配lastIndex 位置开始必须紧接着匹配

su 是 ES2018+ 才稳定的,老环境注意兼容。

不带 g 和带 gmatch() 的返回结构不同,这是常见坑:

"abc123".match(/\d+/);    // ["123", index: 3, ...]
"abc123".match(/\d+/g);   // ["123"]

g 时没有分组信息,需要分组请用 matchAll()

最容易忽略的状态问题

gy 的正则是有状态的,反复调用 exec()test() 时会受 lastIndex 影响。

const reg = /\d/g;
console.log(reg.test("1")); // true
console.log(reg.test("1")); // false  ← 反直觉

所以很多”同一个正则第一次对、第二次不对”的问题,本质上不是正则写错,而是对象状态在移动。

详见:regex-lastindex-pitfall

贪婪与非贪婪

默认量词是贪婪的,会尽可能多吃字符:

"<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>"]  ← 正确

详见:regex-greedy-vs-non-greedy

零宽断言(Lookahead / Lookbehind)

断言匹配的是”位置条件”,不消耗字符。

  • (?=...) — 正向先行:后面必须跟 ...
  • (?!...) — 负向先行:后面不能跟 ...
  • (?<=...) — 正向后行:前面必须跟 ...
  • (?<!...) — 负向后行:前面不能跟 ...
"12px".match(/\d+(?=px)/)[0];        // "12"(不含 px)
"$199".match(/(?<=\$)\d+/)[0];       // "199"(不含 $)

详见:regex-lookahead-lookbehind

分组进阶

普通捕获组

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 状态gtest() / 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 文本时

这类情况往往更适合专门解析逻辑,而不是继续堆正则。

延伸阅读

创建于 2025/1/1 更新于 2026/5/27