JavaScript 正则:RegExp 对象、test/exec/match/replace/matchAll 差异

两种创建方式,一个隐藏差异

JS 里正则既可以用字面量 /pattern/flags 创建,也可以 new RegExp("pattern", "flags")。字面量在解析期编译,性能好、写法直观,是默认选择;构造函数接收字符串,适合模式来自变量的场景(用户输入关键词拼接、配置下发)。用构造函数时别忘了字符串的转义再转义问题:要表达 \d 得写 new RegExp("\\d"),双层反斜杠极易写错。

const re1 = /DD\d{6}/;                    // 字面量:静态模式首选
const kw = "8812";
const re2 = new RegExp(`DD${kw}\\d{2}`);  // 动态拼接场景
console.log(re1.test("订单 DD881201 已发货"));  // true

六个 API,各司其职

正则相关 API 分布在 RegExp 与 String 两侧,先给全景表,再逐个拆:

API 归属 返回什么 受 g 标志影响
test() RegExp 布尔值 移动 lastIndex(陷阱)
exec() RegExp 匹配详情或 null 循环推进 lastIndex
match() String 数组或 null 有 g 返回所有整匹配、丢分组
matchAll() String 迭代器 必须带 g,保留分组
replace() String 替换后的新串 有 g 全部替换
search() / split() String 位置 / 数组 不受影响

记忆主线:RegExp 系的方法带状态,String 系的方法带惊喜。

test 与 exec:状态机双雄

test 回答"有没有",exec 回答"是什么、在哪、分组是什么"。它们共享 lastIndex 状态,配 g 使用时每次调用都从上次的终点继续——这既是特性也是陷阱:

const re = /(\d{4})-(\d{2})/g;
const log = "上线 2024-10-26,复盘 2024-11-02";
let m;
while ((m = re.exec(log)) !== null) {
  console.log(m[1] + "年" + m[2] + "月");   // 2024年10月,然后 2024年11月
}

exec 每次返回带 groups、index 的完整详情,循环到 null 为止——这是 ES2020 之前提取全部分组的标准姿势。而 match 加了 g 之后只返回整匹配数组,分组全部丢失:

console.log("上线 2024-10-26".match(/(\d{4})-(\d{2})/g));    // ['2024-10']
console.log("上线 2024-10-26".match(/(\d{4})-(\d{2})/));     // 带 groups 的完整数组

同一个方法,有无 g 返回结构完全不同——JS 正则 bug 榜第一名。需要"全部匹配 + 分组"时,用 matchAll:

for (const m of "复盘 2024-11-02".matchAll(/(\d{4})-(\d{2})/g)) {
  console.log(m[2]);   // 11
}

replace:分组重组与函数回调

replace 支持三种替换语法:$1 数字分组、$<名字> 命名分组、传入函数做任意加工。日期格式翻转是 $1 的经典舞台:

const date = "2024-10-26";
console.log(date.replace(/(\d{4})-(\d{2})-(\d{2})/, "$2/$3/$1"));
// 10/26/2024

const line = "用户 u_7788 超时 3000ms";
console.log(line.replace(/超时 (\d+)ms/, (_, ms) => `耗时 ${(ms / 1000).toFixed(1)}s`));
// 用户 u_7788 耗时 3.0s

替换函数的参数依次是整匹配、各分组、偏移量、原文,签名记不清时用剩余参数接住再调试。能在替换函数里做的复杂逻辑,别硬塞进正则——这是 JS 侧保持正则简洁的诀窍。

学完立刻能用的三个习惯

  1. 提取用 matchAll,判断用 test,替换用 replace——先按需求选 API,再写模式。
  2. 带全局标志的正则对象不做模块级复用(lastIndex 会串场),要复用先重置 re.lastIndex = 0。
  3. 用命名组 (?<year>...) 配 m.groups.year,把魔法下标从代码里赶出去。

避坑提醒:动态拼接正则时若模式片段来自用户输入,必须先 str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") 转义,否则用户输入一个 ( 就能让整个正则抛语法错误,输入 .* 则可能引爆性能问题。把用户输入当正则用等于正则注入,务必转义。

想系统补 JavaScript 基础再回头看本章,可参考本站《廖雪峰 JavaScript 教程》:JavaScript 教程。

split 接受正则时能按"一类分隔符"切分,比字符串版强大得多:

"a, b,,  c".split(/,\s*/);          // ['a', 'b', '', '  c']
"a, b,, c".split(/[\s,]+/);         // ['a', 'b', 'c']

第一行只处理逗号后的空白,连续逗号仍产生空串;第二行把空白和逗号合并当成"一个分隔符",结果立刻干净。分隔符不规则时,用字符类加量词收编它们。若捕获组出现在分隔模式里,分组内容会被保留在结果数组中——想保留分隔符时的技巧。

search 则是"只要位置"的最快答案:

const i = "订单 DD881202 已发货".search(/DD\d{6}/);
console.log(i);                     // 3

配合 slice 可实现"从命中处截取"的轻量解析。三件套的选型口诀:判断有没有用 test,要知道在哪用 search,要拿内容用 matchAll。

综合小技:一个 10 行的迷你模板引擎

把命名组、matchAll、替换函数串起来,能写出一个够用的微模板工具——体会 JS 正则三件套的配合:

function render(tpl, data) {
  return tpl.replace(/\$\{(\w+)\}/g, (_, key) => data[key] ?? "");
}
console.log(render("您好 ${name},订单 ${oid} 已发货", { name: "老王", oid: "DD881202" }));
// 输出:您好 老王,订单 DD881202 已发货

\$\{(\w+)\} 捕获占位符里的键名,替换回调从数据对象取值,?? "" 让缺失字段安静降级为空串。十几行代码,覆盖了 90% 的"填空"需求——剩下 10%(嵌套对象、格式化)就老实上模板库。这个小例子也是 JS 正则知识点的最佳复习场:转义、分组、全局标志、回调签名,一个不落。

参考资源

本章由 WorkBuddy 原创撰写,示例均经实际验证。