JavaScript正则表达式之选择、分组与引用深度解析
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">一、选择(|):实现 “或” 逻辑匹配</a></li><ul class="second_class_ul"><li><a href="#_lab2_0_0">1. 核心定义</a></li><li><a href="#_lab2_0_1">2. 语法规则</a></li><li><a href="#_lab2_0_2">3. 代码案例与运行结果</a></li><ul class="third_class_ul"><li><a href="#_label3_0_2_0">案例 1:基础选择匹配(匹配多个固定字符串)</a></li><li><a href="#_label3_0_2_1">案例 2:选择符与分组配合(限定选择范围)</a></li><li><a href="#_label3_0_2_2">案例 3:选择符的左优先匹配特性</a></li></ul><li><a href="#_lab2_0_3">4. 实战场景:匹配多种日期格式</a></li><ul class="third_class_ul"></ul></ul><li><a href="#_label1">二、分组(()):将多个字符视为一个整体</a></li><ul class="second_class_ul"><li><a href="#_lab2_1_4">1. 核心定义</a></li><ul class="third_class_ul"></ul><li><a href="#_lab2_1_5">2. 分组的分类与语法</a></li><ul class="third_class_ul"></ul><li><a href="#_lab2_1_6">3. 代码案例与运行结果</a></li><ul class="third_class_ul"><li><a href="#_label3_1_6_3">案例 1:普通捕获组(批量限定量词)</a></li><li><a href="#_label3_1_6_4">案例 2:非捕获组(仅整体控制,不捕获结果)</a></li><li><a href="#_label3_1_6_5">案例 3:分组嵌套(复杂子模式控制)</a></li></ul><li><a href="#_lab2_1_7">4. 实战场景:提取 URL 中的域名和路径</a></li><ul class="third_class_ul"></ul></ul><li><a href="#_label2">三、引用(\n 与 $n):复用分组捕获结果</a></li><ul class="second_class_ul"><li><a href="#_lab2_2_8">1. 核心定义</a></li><ul class="third_class_ul"></ul><li><a href="#_lab2_2_9">2. 语法规则</a></li><ul class="third_class_ul"></ul><li><a href="#_lab2_2_10">3. 代码案例与运行结果</a></li><ul class="third_class_ul"><li><a href="#_label3_2_10_6">案例 1:正则内引用(\n):匹配重复内容</a></li><li><a href="#_label3_2_10_7">案例 2:代码内引用($n):动态替换内容</a></li><li><a href="#_label3_2_10_8">案例 3:代码内引用(result ):提取分组详情</a></li><li><a href="#_label3_2_10_9">案例 4:引用的边界场景(越界引用)</a></li></ul><li><a href="#_lab2_2_11">4. 实战场景:修复 HTML 标签(闭合不匹配的标签)</a></li><ul class="third_class_ul"></ul></ul><li><a href="#_label3">四、选择、分组、引用的组合实战</a></li><ul class="second_class_ul"><li><a href="#_lab2_3_12">场景:验证并提取 “带区号的固定电话”</a></li><ul class="third_class_ul"></ul><li><a href="#_lab2_3_13">实现代码</a></li><ul class="third_class_ul"></ul><li><a href="#_lab2_3_14">运行结果</a></li><ul class="third_class_ul"></ul></ul><li><a href="#_label4">五、避坑指南</a></li><ul class="second_class_ul"></ul><li><a href="#_label5">总结</a></li><ul class="second_class_ul"></ul></ul></div><p>在正则表达式中,“选择、分组、引用” 是实现复杂匹配逻辑的核心语法,能够将多个简单规则组合成灵活的匹配模板。</p><p class="maodian"><a name="_label0"></a></p><h2>一、选择(|):实现 “或” 逻辑匹配</h2>
<p>选择符(<code>|</code>)用于匹配 “多个模式中的任意一个”,相当于逻辑中的 “或”(OR),是正则中实现多规则匹配的基础。</p>
<p class="maodian"><a name="_lab2_0_0"></a></p><p class="maodian"><a name="_lab2_1_4"></a></p><p class="maodian"><a name="_lab2_2_8"></a></p><h3>1. 核心定义</h3>
<p>选择符 <code>|</code> 分隔两个或多个候选模式,正则会依次尝试匹配每个模式,只要有一个模式匹配成功,整个正则就返回匹配结果。</p>
<p class="maodian"><a name="_lab2_0_1"></a></p><p class="maodian"><a name="_lab2_2_9"></a></p><h3>2. 语法规则</h3>
<ul><li>基础语法:<code>pattern1|pattern2|pattern3</code>(匹配 pattern1、pattern2、pattern3 中的任意一个);</li><li>优先级:<code>|</code> 的优先级最低,若需限定选择范围,需配合分组 <code>()</code> 使用(否则会匹配 “左侧全量 + 右侧全量”);</li><li>匹配顺序:正则会从左到右尝试候选模式,一旦匹配成功就停止后续尝试(即 “左优先匹配”)。</li></ul>
<p class="maodian"><a name="_lab2_0_2"></a></p><p class="maodian"><a name="_lab2_1_6"></a></p><p class="maodian"><a name="_lab2_2_10"></a></p><h3>3. 代码案例与运行结果</h3>
<p class="maodian"><a name="_label3_0_2_0"></a></p><h4>案例 1:基础选择匹配(匹配多个固定字符串)</h4>
<p>javascript</p>
<p>运行</p>
<div class="jb51code"><pre class="brush:js;">// 匹配 "apple"、"banana"、"orange" 中的任意一个
const reg = /apple|banana|orange/;
const fruits = ["apple", "banana", "orange", "grape"];
fruits.forEach(fruit => {
console.log(`${fruit}: ${reg.test(fruit)}`);
});
// 运行结果:
// apple: true(匹配 "apple")
// banana: true(匹配 "banana")
// orange: true(匹配 "orange")
// grape: false(无匹配)
</pre></div>
<p class="maodian"><a name="_label3_0_2_1"></a></p><h4>案例 2:选择符与分组配合(限定选择范围)</h4>
<p>若不使用分组,选择符会匹配 “左侧全量 + 右侧全量”,导致逻辑错误,需用 <code>()</code> 限定选择范围:</p>
<p>javascript</p>
<p>运行</p>
<div class="jb51code"><pre class="brush:js;">const str1 = "I like apples";
const str2 = "I like oranges";
const str3 = "I like grape";
// 错误写法:未分组,匹配 "I like app" 或 "les"(逻辑不符合预期)
const wrongReg = /I like app|les/;
console.log(wrongReg.test(str1)); // true(错误匹配 "I like app")
console.log(wrongReg.test(str2)); // false(无匹配)
// 正确写法:用 () 限定选择范围,匹配 "I like apple" 或 "I like orange"
const rightReg = /I like (apple|orange)/;
console.log(rightReg.test(str1)); // true(匹配 "I like apple")
console.log(rightReg.test(str2)); // true(匹配 "I like orange")
console.log(rightReg.test(str3)); // false(无匹配)
</pre></div>
<p class="maodian"><a name="_label3_0_2_2"></a></p><h4>案例 3:选择符的左优先匹配特性</h4>
<p>正则会从左到右尝试候选模式,一旦匹配成功就停止,即使右侧有更优匹配:</p>
<p>javascript</p>
<p>运行</p>
<div class="jb51code"><pre class="brush:js;">// 匹配 "abc" 或 "abcd"(左优先:先尝试 "abc")
const reg = /abc|abcd/;
const str = "abcd";
// 匹配结果:优先匹配左侧的 "abc",而非完整的 "abcd"
console.log(str.match(reg)); // ["abc", index: 0, input: "abcd", groups: undefined]
</pre></div>
<p class="maodian"><a name="_lab2_0_3"></a></p><h3>4. 实战场景:匹配多种日期格式</h3>
<p>用选择符匹配 <code>yyyy-mm-dd</code>、<code>yyyy/mm/dd</code>、<code>yyyy.mm.dd</code> 三种日期格式:</p>
<p>javascript</p>
<p>运行</p>
<div class="jb51code"><pre class="brush:js;">// 匹配三种日期格式(年-月-日、年/月/日、年.月.日)
const dateReg = /^\d{4}(-|\/|\.)\d{2}\1\d{2}$/;
// 注:\1 是分组引用,后续会讲解,此处用于保证分隔符一致(如 "-" 前后一致)
console.log(dateReg.test("2024-05-20")); // true(匹配 "-" 格式)
console.log(dateReg.test("2024/05/20")); // true(匹配 "/" 格式)
console.log(dateReg.test("2024.05.20")); // true(匹配 "." 格式)
console.log(dateReg.test("2024-05/20")); // false(分隔符不一致)
</pre></div>
<p class="maodian"><a name="_label1"></a></p><h2>二、分组(()):将多个字符视为一个整体</h2>
<p>分组符(<code>()</code>)用于将多个字符或子模式 “打包” 成一个整体,实现 “批量控制”(如批量限定量词、批量参与选择),是正则中实现复杂规则的核心。</p>
<h3>1. 核心定义</h3>
<p>分组符 <code>()</code> 会将括号内的内容视为一个 “子模式”,可以对这个子模式整体应用量词、选择符等规则,同时分组会捕获匹配结果(用于后续引用)。</p>
<p class="maodian"><a name="_lab2_1_5"></a></p><h3>2. 分组的分类与语法</h3>
<p>根据是否捕获匹配结果,分组可分为 “捕获组” 和 “非捕获组”,常用分类如下:</p>
<table><tbody><tr><th>分组类型</th><th>语法</th><th>描述</th></tr><tr><td>普通捕获组</td><td><code>(pattern)</code></td><td>捕获括号内 pattern 的匹配结果,可通过 <code>\n</code>(正则内)或 <code>result</code>(代码内)引用</td></tr><tr><td>非捕获组</td><td><code>(?:pattern)</code></td><td>仅将 pattern 视为整体,不捕获匹配结果(节省性能,无需引用时推荐)</td></tr><tr><td>正向前瞻分组</td><td><code>(?=pattern)</code></td><td>不捕获结果,仅判断当前位置后是否满足 pattern(前面文章已讲,此处不展开)</td></tr><tr><td>负向前瞻分组</td><td><code>(?!pattern)</code></td><td>不捕获结果,仅判断当前位置后是否不满足 pattern</td></tr></tbody></table>
<h3>3. 代码案例与运行结果</h3>
<p class="maodian"><a name="_label3_1_6_3"></a></p><h4>案例 1:普通捕获组(批量限定量词)</h4>
<p>将 <code>ab</code> 视为一个整体,匹配其重复 2 次的场景:</p>
<p>javascript</p>
<p>运行</p>
<div class="jb51code"><pre class="brush:js;">// 分组前:匹配 "a" 重复 2 次 + "b"(即 "aab")
const noGroupReg = /a{2}b/;
// 分组后:匹配 "ab" 重复 2 次(即 "abab")
const groupReg = /(ab){2}/;
console.log(noGroupReg.test("aab")); // true(分组前,匹配 "aab")
console.log(noGroupReg.test("abab")); // false(分组前,不匹配)
console.log(groupReg.test("abab")); // true(分组后,匹配 "abab")
console.log(groupReg.test("aab")); // false(分组后,不匹配)
</pre></div>
<p class="maodian"><a name="_label3_1_6_4"></a></p><h4>案例 2:非捕获组(仅整体控制,不捕获结果)</h4>
<p>当不需要引用分组结果时,用 <code>(?:pattern)</code> 定义非捕获组,避免不必要的结果存储,提升性能:</p>
<p>javascript</p>
<p>运行</p>
<div class="jb51code"><pre class="brush:js;">// 非捕获组:匹配 "apple" 或 "banana",不捕获结果
const nonCaptureReg = /(?:apple|banana)/;
const str = "I like apple";
// 匹配结果:仅返回整体匹配内容,无分组捕获结果
const result = str.match(nonCaptureReg);
console.log(result); // ["apple", index: 7, input: "I like apple", groups: undefined]
console.log(result); // undefined(非捕获组,无分组结果)
// 普通捕获组:捕获 "apple" 或 "banana" 的结果
const captureReg = /(apple|banana)/;
const captureResult = str.match(captureReg);
console.log(captureResult); // "apple"(普通捕获组,可获取分组结果)
</pre></div>
<p class="maodian"><a name="_label3_1_6_5"></a></p><h4>案例 3:分组嵌套(复杂子模式控制)</h4>
<p>分组支持嵌套,嵌套分组的捕获顺序为 “左括号出现的顺序”(即第 1 个左括号为组 1,第 2 个为组 2,以此类推):</p>
<p>javascript</p>
<p>运行</p>
<div class="jb51code"><pre class="brush:js;">// 嵌套分组:匹配 "a(bc(d))"(组 1:"bc(d)",组 2:"d")
const reg = /a(bc(d))/;
const str = "abcde";
const result = reg.exec(str);
console.log(result); // "abcd"(整体匹配结果)
console.log(result); // "bcd"(第 1 个左括号对应的分组结果)
console.log(result); // "d"(第 2 个左括号对应的分组结果)
</pre></div>
<p class="maodian"><a name="_lab2_1_7"></a></p><h3>4. 实战场景:提取 URL 中的域名和路径</h3>
<p>用分组提取 URL(如 <code>https://www.baidu.com/s?wd=regex</code>)中的 “域名”(<code>www.baidu.com</code>)和 “路径”(<code>/s</code>):</p>
<p>javascript</p>
<p>运行</p>
<div class="jb51code"><pre class="brush:js;">function extractUrlParts(url) {
// 分组1:域名(如 www.baidu.com),分组2:路径(如 /s)
const reg = /https?:\/\/([^\/]+)(\/[^?]*)/;
const result = url.match(reg);
if (result) {
return {
domain: result, // 第 1 组:域名
path: result // 第 2 组:路径
};
}
return null;
}
const url = "https://www.baidu.com/s?wd=regex";
const parts = extractUrlParts(url);
console.log(parts);
// 运行结果:
// { domain: "www.baidu.com", path: "/s" }
</pre></div>
<p class="maodian"><a name="_label2"></a></p><h2>三、引用(\n 与 $n):复用分组捕获结果</h2>
<p>引用是基于分组的延伸特性,用于 “复用之前分组的捕获结果”,分为 “正则内引用”(<code>\n</code>)和 “代码内引用”(<code>$n</code> 或 <code>result</code>),能够实现 “重复匹配”“动态替换” 等高级功能。</p>
<h3>1. 核心定义</h3>
<ul><li><strong>正则内引用</strong>:用 <code>\n</code>(n 为分组序号,从 1 开始)在正则表达式内部引用第 n 个分组的捕获结果;</li><li><strong>代码内引用</strong>:在代码中(如 <code>replace</code> 方法)用 <code>$n</code> 引用第 n 个分组的结果,或通过 <code>exec</code>/<code>match</code> 的返回数组 <code>result</code> 获取。</li></ul>
<h3>2. 语法规则</h3>
<ul><li>分组序号:从左到右数左括号的顺序,第 1 个左括号为组 1,第 2 个为组 2,以此类推;</li><li>非捕获组:<code>(?:pattern)</code> 不捕获结果,无法被引用;</li><li>引用范围:<code>\n</code> 仅在正则内部生效,<code>$n</code> 仅在 <code>replace</code> 等字符串方法中生效,<code>result</code> 适用于所有代码场景。</li></ul>
<h3>3. 代码案例与运行结果</h3>
<p class="maodian"><a name="_label3_2_10_6"></a></p><h4>案例 1:正则内引用(\n):匹配重复内容</h4>
<p>用 <code>\1</code> 引用第 1 组的结果,匹配 “前后重复的单词”(如 <code>hello hello</code>):</p>
<p>javascript</p>
<p>运行</p>
<div class="jb51code"><pre class="brush:js;">// 分组1:匹配单词(\w+),\1:引用分组1的结果,匹配重复单词
const reg = /(\w+)\s+\1/;
const str1 = "hello hello";
const str2 = "hello world";
const str3 = "apple apple banana";
console.log(reg.test(str1)); // true(匹配 "hello hello")
console.log(reg.test(str2)); // false(无重复单词)
console.log(str3.match(reg)); // ["apple apple", "apple"](匹配 "apple apple",分组1结果为 "apple")
</pre></div>
<p class="maodian"><a name="_label3_2_10_7"></a></p><h4>案例 2:代码内引用($n):动态替换内容</h4>
<p>在 <code>replace</code> 方法中用 <code>$1</code>、<code>$2</code> 引用分组结果,实现 “格式化手机号”(中间 4 位打码):</p>
<p>javascript</p>
<p>运行</p>
<div class="jb51code"><pre class="brush:js;">// 分组1:前 3 位数字,分组2:后 4 位数字
const phoneReg = /(\d{3})\d{4}(\d{4})/;
const phone = "13812345678";
// $1 引用分组1(138),$2 引用分组2(5678),中间用 **** 替换
const formattedPhone = phone.replace(phoneReg, "$1****$2");
console.log(formattedPhone); // 运行结果:138****5678
</pre></div>
<p class="maodian"><a name="_label3_2_10_8"></a></p><h4>案例 3:代码内引用(result ):提取分组详情</h4>
<p>通过 <code>exec</code> 或 <code>match</code> 的返回数组获取分组结果,提取 “身份证号中的出生日期”(第 7-14 位):</p>
<p>javascript</p>
<p>运行</p>
<div class="jb51code"><pre class="brush:js;">// 身份证号规则:18位,第 7-14 位为出生日期(yyyyMMdd)
const idCardReg = /^(\d{6})(\d{8})(\d{4})$/;
const idCard = "110101200001011234";
const result = idCardReg.exec(idCard);
if (result) {
console.log("地区码:", result); // 地区码:110101(第 1 组)
console.log("出生日期:", result); // 出生日期:20000101(第 2 组)
console.log("校验码:", result); // 校验码:1234(第 3 组)
}
// 运行结果:
// 地区码: 110101
// 出生日期: 20000101
// 校验码: 1234
</pre></div>
<p class="maodian"><a name="_label3_2_10_9"></a></p><h4>案例 4:引用的边界场景(越界引用)</h4>
<p>若引用的分组序号不存在(如引用第 3 组,但仅定义了 2 个分组),<code>\n</code> 会被视为普通字符:</p>
<p>javascript</p>
<p>运行</p>
<div class="jb51code"><pre class="brush:js;">// 仅定义 2 个分组,引用第 3 组(越界)
const reg = /(\d{2})-(\d{2})\3/;
const str1 = "05-203"; // 末尾为 "3",与 \3(视为普通字符 "3")匹配
const str2 = "05-204"; // 末尾为 "4",不匹配
console.log(reg.test(str1)); // true(\3 视为 "3",匹配 "05-203")
console.log(reg.test(str2)); // false(不匹配)
</pre></div>
<p class="maodian"><a name="_lab2_2_11"></a></p><h3>4. 实战场景:修复 HTML 标签(闭合不匹配的标签)</h3>
<p>用引用修复 “标签名不匹配” 的 HTML(如 <code><div>content</p></code>),将闭合标签改为与开始标签一致:</p>
<p>javascript</p>
<p>运行</p>
<div class="jb51code"><pre class="brush:js;">function fixHtmlTag(html) {
// 分组1:匹配开始标签名(如 div),替换闭合标签为 $1(与开始标签一致)
const reg = /<(\w+)>([\s\S]*?)<\/\w+>/;
return html.replace(reg, "<$1>$2</$1>");
}
const wrongHtml = "<div>这是错误的标签</p>";
const fixedHtml = fixHtmlTag(wrongHtml);
console.log(fixedHtml); // 运行结果:<div>这是错误的标签</div>
</pre></div>
<p class="maodian"><a name="_label3"></a></p><h2>四、选择、分组、引用的组合实战</h2>
<p class="maodian"><a name="_lab2_3_12"></a></p><h3>场景:验证并提取 “带区号的固定电话”</h3>
<p>固定电话规则:</p>
<ul><li>区号:3-4 位数字,可选(如 <code>010-</code> 或 <code>021-</code>);</li><li>号码:7-8 位数字;</li><li>分隔符:区号与号码之间用 <code>-</code> 或 (空格)分隔;</li><li>整体格式:[区号][分隔符][号码](区号可选)。</li></ul>
<p class="maodian"><a name="_lab2_3_13"></a></p><h3>实现代码</h3>
<p>javascript</p>
<p>运行</p>
<div class="jb51code"><pre class="brush:js;">function validateAndExtractLandline(phone) {
// 分组1:区号(可选,3-4位数字),分组2:分隔符(- 或 空格),分组3:号码(7-8位数字)
const reg = /^(?:(\d{3,4})([- ]))?(\d{7,8})$/;
const result = phone.match(reg);
if (!result) {
return "固定电话格式错误";
}
return {
fullPhone: result,
areaCode: result || "无区号", // 区号可选,无则返回 "无区号"
number: result
};
}
// 测试案例
const phones = [
"010-12345678",// 带区号(4位)+ "-" 分隔
"021 87654321",// 带区号(3位)+ 空格分隔
"1234567", // 无区号(7位号码)
"010-1234567" // 错误(号码仅 7 位,但区号+分隔符存在,需 8 位号码)
];
phones.forEach(phone => {
console.log(phone + ":", validateAndExtractLandline(phone));
});
</pre></div>
<p class="maodian"><a name="_lab2_3_14"></a></p><h3>运行结果</h3>
<p>plaintext</p>
<div class="jb51code"><pre class="brush:js;">010-12345678: { fullPhone: '010-12345678', areaCode: '010', number: '12345678' }
021 87654321: { fullPhone: '021 87654321', areaCode: '021', number: '87654321' }
1234567: { fullPhone: '1234567', areaCode: '无区号', number: '1234567' }
010-1234567: 固定电话格式错误
</pre></div>
<p class="maodian"><a name="_label4"></a></p><h2>五、避坑指南</h2>
<ul><li><strong>选择符的优先级问题</strong>:<code>|</code> 优先级最低,若需限定选择范围,必须配合分组 <code>()</code>,否则会匹配 “左侧全量 + 右侧全量”(如 <code>/a|bc/</code> 匹配 <code>a</code> 或 <code>bc</code>,而非 <code>ab</code> 或 <code>ac</code>);</li><li><strong>分组的捕获与性能</strong>:无需引用分组结果时,优先用非捕获组 <code>(?:pattern)</code>,避免存储无用的捕获结果,提升正则匹配效率;</li><li><strong>引用的序号规则</strong>:分组序号按 “左括号出现顺序” 计数,嵌套分组需仔细数清序号(如 <code>(a(b(c)))</code> 中,组 1:<code>a(b(c))</code>,组 2:<code>b(c)</code>,组 3:<code>c</code>);</li><li><strong>全局匹配的引用问题</strong>:用 <code>g</code> 修饰符时,<code>exec</code> 会循环匹配,每次匹配的分组结果需通过 <code>result</code> 实时获取,不可复用之前的分组结果。</li></ul>
<p class="maodian"><a name="_label5"></a></p><h2>总结</h2>
<p>选择(<code>|</code>)、分组(<code>()</code>)、引用(<code>\n</code>/<code>$n</code>)是正则表达式的 “三驾马车”:</p>
<ul><li><strong>选择</strong>:实现多规则 “或” 逻辑,解决 “匹配多个候选模式” 的需求;</li><li><strong>分组</strong>:将多个字符打包成整体,支持批量控制和结果捕获,是复杂规则的基础;</li><li><strong>引用</strong>:复用分组结果,实现重复匹配和动态替换,提升正则的灵活性。</li></ul>
頁:
[1]