跳到主要内容

元字符与量词

上一篇介绍了字符类和锚点,本文继续深入正则的"语法骨架":元字符、量词、分组与反向引用、或操作、转义。这些是写出复杂模式所必须的工具,也是面试中"手写正则"题的核心考察点。

元字符

元字符是正则中有特殊含义的字符,不再是字面量本身。一共需要记住以下几个:

元字符含义
\转义
^行首锚点 / 字符类取反
$行尾锚点
.除换行符外任意字符
``
? * +量词
()分组
[]字符类
{}量词(重复次数)

要在正则中匹配这些字符的字面量,需要用 \ 转义:

/\$/.test("$1");      // true,匹配字面量 $
/\(\)/.test("()"); // true,匹配字面量 ()
/\d+\.\d+/.test("3.14"); // true,匹配字面量小数点

面试易错点:在字符类 [] 内,大多数元字符会自动失去特殊含义,无需转义。例如 [.*+?] 是一个匹配 .*+? 中任意一个的字符类。但 ]\^(开头)、-(非末尾)仍需小心。

量词

量词用来描述前一个元素出现的次数。

量词含义等价写法
*出现 0 次或多次{0,}
+出现 1 次或多次{1,}
?出现 0 次或 1 次{0,1}
{n}恰好 n 次
{n,}至少 n 次
{n,m}n 到 m 次(含两端)
/a*/.test("");       // true,0 次也算
/a+/.test(""); // false,至少 1 次
/colou?r/.test("color"); // true,u 可有可无
/\d{4}/.test("2023abc"); // true,连续 4 位数字
/\d{2,4}/.test("123"); // true,2~4 位

注意 {n,m}不能有空格{2, 4} 不会被识别为量词,而是匹配字面量字符串 {2, 4}

贪婪、懒惰、占有

量词默认是贪婪的:会尽可能多地匹配字符。

贪婪模式

"aabaa".match(/a.*a/); // ['aabaa']
// .* 尽可能多匹配,到末尾后回溯找到最后一个 a

懒惰模式

在量词后加 ? 变成懒惰模式:尽可能少地匹配。

贪婪懒惰
**?
++?
???
{n,m}{n,m}?
"aabaa".match(/a.*?a/); // ['aa']
// 尽可能少匹配,找到第一个 a 后立即匹配下一个 a

经典的 HTML 标签匹配对比:

const html = '<div>a</div><div>b</div>';
html.match(/<div>.*<\/div>/); // ['<div>a</div><div>b</div>'] 贪婪
html.match(/<div>.*?<\/div>/); // ['<div>a</div>'] 懒惰

面试经典题:为什么 /".*"/.test('a="1" b="2"') 会匹配到整个 "1" b="2" 而不是 "1"?答案就是贪婪。改为 ".*?" 即可匹配到 "1"

占有模式

在量词后加 + 变成占有模式(ES2025):尽可能多匹配,但不会回溯。一旦匹配到的字符就不会再交还。

// 占有模式:匹配失败时不回溯,整体失败
/a*+/; // 等价于 (?>a*)

占有模式主要用于性能优化,避免某些灾难性回溯。目前主流浏览器逐步支持中,了解概念即可。具体原理与回溯的关系,见 断言与回溯

分组 ()

括号将多个元素当成一个整体,配合量词使用:

/(ab)+/.test("abab"); // true,ab 作为整体重复
/ab+/.test("abbb"); // true,只有 b 重复

捕获组

() 默认是捕获组,匹配到的内容会被记录,可在后续通过 $1$2 引用,或通过 match 返回结果访问:

const m = "2023-07-01".match(/(\d{4})-(\d{2})-(\d{2})/);
console.log(m[0]); // '2023-07-01' 整体匹配
console.log(m[1]); // '2023' 第一个捕获组
console.log(m[2]); // '07'
console.log(m[3]); // '01'

非捕获组 (?:...)

不需要引用时,用 (?:...) 表示非捕获组,只分组不捕获,性能略好且不占用编号:

const m = "2023-07-01".match(/(?:\d{4})-(\d{2})-(\d{2})/);
console.log(m[1]); // '07',第一个分组变成第二个,因为 (?:...) 不占编号

工程实践:不需要捕获时优先用 (?:...),可读性和性能都更好。

命名捕获组 (?<name>...)

ES2018 引入,给捕获组起名字,比数字编号更清晰:

const m = "2023-07-01".match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);
console.log(m.groups.year); // '2023'
console.log(m.groups.month); // '07'
console.log(m.groups.day); // '01'

更详细的用法见 进阶用法

反向引用

捕获组可以被同一正则中的后续部分引用,形式为 \1\2(编号对应第 n 个捕获组):

// 匹配前后引号一致
/^(['"]).*\1$/.test('"hello"'); // true
/^(['"]).*\1$/.test("'hello'"); // true
/^(['"]).*\1$/.test('"hello\''); // false,前后引号不一致
// 匹配回文样式的单词
/(\w)\1/.test("book"); // true,oo 是两个相同字符

命名捕获组对应的反向引用是 \k<name>

/^(?<quote>['"]).*\k<quote>$/.test('"abc"'); // true

经典面试题:如何用一个正则匹配 HTML 标签的开始与结束(标签名一致)?答案就是反向引用: /<(\w+)>.*<\/\1>//<(?<tag>\w+)>.*<\/\k<tag>>/

|

| 表示"或",优先级在所有运算中最低。注意它只分隔左右两边,不会自动延伸到整行:

/cat|dog/.test("cat"); // true
/cat|dog/.test("dog"); // true

// 误区:/^abc|def$/ 不是 "整行是 abc 或整行是 def"
// 而是 "以 abc 开头" 或 "以 def 结尾"
/^abc|def$/.test("abcxyz"); // true,匹配开头 abc
/^abc|def$/.test("xyzdef"); // true,匹配结尾 def

// 正确写法:用分组限定范围
/^(abc|def)$/.test("abcxyz"); // false
/^(abc|def)$/.test("abc"); // true

这是一个非常常见的错误:以为 | 会让整行都参与"或",实际上它的作用范围只到最近的边界。

转义

要用字面量匹配元字符本身,必须用 \ 转义:

// 匹配 a.b(点号是字面量)
/a\.b/.test("a.b"); // true
/a\.b/.test("axb"); // false

// 匹配文件路径中的反斜杠
/C:\\Users/.test("C:\\Users"); // true

// 匹配正则字符串本身,比如 /abc/
/\/abc\//.test("/abc/"); // true

转义对照

字面量字符转义写法
.\.
*\*
+\+
?\?
^\^
$\$
``
( )\( \)
[ ]\[ \]
{ }\{ \}
/(字面量分隔符)\/
\\\

RegExp 构造函数时的双重转义

// 字面量写法
const re1 = /\d+\./;

// 等价的构造函数写法
const re2 = new RegExp("\\d+\\.");

字符串字面量 "\\d" 解析后是 \d,再交给正则引擎才能正确识别为数字简写。这是面试中常考的细节。

综合示例:解析 query 字符串

function parseQuery(qs) {
const result = {};
const re = /(?:^|&)([^&=]+)=([^&]*)/g;
let m;
while ((m = re.exec(qs))) {
result[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
}
return result;
}
parseQuery("a=1&b=2&c=hello");
// { a: '1', b: '2', c: 'hello' }
  • (?:^|&):非捕获组,匹配开头或 &
  • ([^&=]+):捕获 key,匹配非 &= 的字符。
  • =:字面量。
  • ([^&]*):捕获 value,匹配任意非 & 字符。
  • g:全局匹配,配合 exec 循环取所有结果。

小结

概念关键点
元字符需要转义才能匹配字面量
量词* + ? {n,m}
贪婪 vs 懒惰默认贪婪,加 ? 变懒惰
分组() 捕获、(?:) 非捕获、(?<name>) 命名
反向引用\1\k<name>
`
转义字面量单反斜杠,RegExp 字符串双反斜杠

下一篇 断言与回溯 将介绍正则中最难也最有区分度的部分:先行/后行断言与回溯原理。