讲解

JavaScript 里创建正则有两种方式:字面量 /pattern/flags(写死在代码里用)和 new RegExp('pattern', 'flags')(模式含变量时用,记得双层转义)。API 分挂在两个对象上:正则对象的 test(返回布尔)和 exec(返回匹配详情或 null);字符串对象的 match、matchAll、replace、replaceAll、split、search。记这张地图比背方法签名有用:判断用 test,取第一个加捕获用 match(无 g),取全部加捕获用 matchAll(带 g),替换用 replace,拆分用 split,找位置用 search。

带 g 或 y 的正则对象是「有状态的」:它内部维护 lastIndex,记录下次从哪里开始找。exec 循环(while ((m = re.exec(s)) !== null))正是靠这个状态逐个推进。坑也随之而来:同一个带 g 的正则对象连续两次 test 同一个字符串,第二次会从上次结束的位置继续找,结果可能从 true 变 false——这曾让无数人怀疑人生。对策很简单:test 场景不要加 g;确实需要全局匹配时,要么每次新建正则,要么用完手动 re.lastIndex = 0。

replace 的替换参数可以是字符串(用 $1、$& 等引用),也可以是函数,函数按 (整体匹配, 组1, 组2, ..., 位置, 原串) 收参,返回什么就替换成什么——需要计算逻辑的替换(打码、换算、大小写转换)都走这条路。最后提醒:字符串是不可变的,replace、replaceAll 都返回新字符串,必须接收返回值。

示例

// 方法地图:判断、取第一个、取全部、替换、拆分、找位置
const re = /(\d+)个/;
const s = '3个苹果和5个橙子';
console.assert(re.test(s)); // 有没有
console.assert(s.match(re)[1] === '3'); // 第一个匹配 + 捕获组
console.assert([...s.matchAll(/(\d+)个/g)].map((m) => m[1]).join(',') === '3,5'); // 全部 + 捕获组
console.assert(s.replace(/(\d+)个/g, '[$1]') === '[3]苹果和[5]橙子');
console.assert('a,b; c'.split(/[,;]\s*/).join('|') === 'a|b|c');
console.assert(s.search(/\d+/) === 0); // 第一次出现的位置

lastIndex 陷阱,亲眼看一次:

// 带 g 的正则对象是有状态的
const buggy = /\d+/g;
console.assert(buggy.test('a1') === true);
console.assert(buggy.test('a1') === false); // 同一个对象第二次调用:lastIndex 已经移动!
console.assert(buggy.test('a1') === true); // 第三次失败后 lastIndex 归零,又变 true

// 对策一:纯判断场景不加 g
const ok = /\d+/;
console.assert(ok.test('a1') && ok.test('a1'));

// 对策二:循环取全部时用 exec + lastIndex,这是它的设计用途
const re2 = /(\w+)=(\w+)/g;
const src = 'a=1 b=2 c=3';
const pairs = [];
let m;
while ((m = re2.exec(src)) !== null) pairs.push(m[1] + ':' + m[2]);
console.assert(pairs.join(',') === 'a:1,b:2,c:3');

replace 函数回调做计算型替换:

// 手机号打码:参数依次是 整体, 组1, 组2, 组3, 位置, 原串
const masked = '张三 13812345678'.replace(/(1\d{2})(\d{4})(\d{4})/, (all, a, b, c) => a + '****' + c);
console.assert(masked === '张三 138****5678');

// 把所有数字翻倍(演示回调里可以做任意计算)
const doubled = '1 个苹果 2 个橙子'.replace(/\d+/g, (n) => String(Number(n) * 2));
console.assert(doubled === '2 个苹果 4 个橙子');

// 字符串不可变:不接收返回值等于白调
const original = 'a1';
original.replace(/\d/, '#');
console.assert(original === 'a1'); // 原串纹丝不动

常见坑

  • 带 g 的正则反复 test:lastIndex 状态导致结果交替真假。test 别加 g;需要全局就用 exec 循环或 matchAll。
  • match 加 g 后找捕获组:加 g 的 match 只返回整体匹配数组。要「全部 + 捕获组」用 [...str.matchAll(re)],且 matchAll 要求必须带 g,否则抛错。
  • 忘记接收 replace 返回值:字符串不可变,replace 不改原串。
  • search 与 g 混用:search 永远从 0 开始且忽略 g,它只回答「第一个匹配在哪」,别指望它和 lastIndex 协作。

小结

test 判断、match/matchAll 提取、replace 替换、split 拆分;带 g 的对象有 lastIndex 状态,replace 回调可算可换。下一节对照 Python 的 re 模块。