☰
JavaScript 正则表达式实战:用捕获组与锚点完整校验 MAC 地址(javascript.info 习题精解)
2026/10/8 12:12:44 网站建设 项目流程
  • 文档/教程
  • 前端

【免费下载链接】en.javascript.info

Modern JavaScript Tutorial

项目地址:https://gitcode.com/gh_mirrors/en/en.javascript.info
点击查看免费下载

MAC 地址(Media Access Control address)是网络接口的物理标识,标准格式由 6 个以冒号分隔的两位十六进制数组成,例如01:32:54:67:89:AB。本文基于现代 JavaScript 教程(Modern JavaScript Tutorial,即本仓库 en.javascript.info)中《正则表达式:捕获组》章节的习题 01-test-mac 任务及其官方题解,完整讲解如何从零构造一个既能识别合法 MAC 地址、又能拒绝一切非法输入的 JavaScript 正则表达式。读完本文,你将掌握「字符类 + 量词 + 捕获组 + 锚点」的组合套路,以及RegExp.prototype.test在实际输入校验中的正确用法。

任务要求:验证整串输入是否为 MAC 地址

原题(task.md)给出如下约束:

  • 一个 MAC 地址由 6 个两位十六进制数组成,数与数之间用冒号:分隔;
  • 典型示例:01:32:54:67:89:AB;
  • 需要编写一个正则表达式,使得regexp.test(str)对合法 MAC 地址返回true,对以下非法输入返回false:
    • 0132546789AB—— 没有冒号分隔;
    • 01:32:54:67:89—— 只有 5 个数字段,合法地址必须恰好 6 段;
    • 01:32:54:67:89:ZZ—— 末尾出现非十六进制字符。

注意这里的关键词是「检查(check)」,即判断整个字符串是否符合格式,而不是在长文本中「查找」出 MAC 地址。这一语义差别直接决定了锚点的使用,稍后会详述。

第一步:定义单个十六进制字节[0-9a-f]{2}

MAC 地址的每一段都是两位十六进制数。十六进制字符集为0-9与a-f(大小写均可),对应的字符类为[0-9a-f]。结合量词{2},两位十六进制数写作:

[0-9a-f]{2}

这能匹配01、AB、ab、3f等任意两位十六进制组合。官方题解特别指出,这里假设正则带有i标志:

[0-9a-f]{2} // 配合标志 i,大小写均可

标志i的含义是忽略大小写(case-insensitive),使A与a等价(详见 01-regexp-introduction/article.md 的 Flags 一节)。没有i时,[0-9a-f]只能匹配小写a–f,大写A–F会被判为非法,因此i标志是让 MAC 校验大小写通用的必要前提。

第二步:用捕获组让量词作用于「冒号 + 数字段」整体

MAC 地址由 6 段组成,若直接写[0-9a-f]{2}:[0-9a-f]{2}:...重复六次,模式会极其冗长。更优雅的做法是利用捕获组(capturing group):把模式的一部分用括号(...)包裹后,括号后面的量词会作用于整个括号内容。

回顾本章核心文章 11-regexp-groups/article.md 的定义:

模式的一部分可以用括号(...)括起来,这被称为「捕获组」,有两个作用:

  1. 允许把匹配的一部分作为独立条目放进结果数组;
  2. 如果在括号后放置量词,量词将作用于括号整体。

MAC 地址的结构可以拆解为:首段[0-9a-f]{2},后面跟5 个:[0-9a-f]{2}。因此把「冒号 + 两位十六进制数」整体括起来并重复 5 次:

[0-9a-f]{2}(:[0-9a-f]{2}){5}

解析:

  • [0-9a-f]{2}—— 第 1 段(两位十六进制数);
  • (:[0-9a-f]{2}){5}—— 捕获组内容为「一个冒号后跟两位十六进制数」,量词{5}令其整体重复 5 次,对应第 2~6 段。

这样,模式恰好描述 1 + 5 = 6 个数字段,{5}精确限定了数量,因此01:32:54:67:89(仅 5 段)无法匹配。

第三步:用锚点^...$锁定整串匹配

到此为止,模式已经能匹配01:32:54:67:89:AB。但仅凭当前模式,regexp.test('...01:32:54:67:89:AB...')(前后有多余字符)也会返回true,因为test只要求字符串中存在匹配即可,而不是整串完全符合。

任务要求的是「检查一个字符串是否是 MAC 地址」,即整个字符串必须从头到尾完全匹配。官方题解指出,实现这一点的标准做法是用锚点把模式包裹起来:

^[0-9a-f]{2}(:[0-9a-f]{2}){5}$
  • ^匹配文本开头;
  • $匹配文本结尾。

锚点成对出现^...$常用于「测试字符串是否完全匹配某格式」的场景,例如校验用户输入是否合规(详见 04-regexp-anchors/article.md 的 "Testing for a full match" 一节,其中用^\d\d:\d\d$校验12:34格式的时间,原理与本任务完全一致)。官方题解也明确说明:

现在需要让匹配覆盖全部文本:从开头开始、在结尾结束。这通过把模式包裹在^...$中实现。

锚点属于「零宽度」断言,它们不消费任何字符,只是强制引擎检查「当前位置是否处于文本边界」,因此不会干扰字符匹配逻辑,只起到限定范围的作用。

完整解答与逐项测试

综合以上三步,官方题解给出的最终正则如下:

let regexp = /^[0-9a-f]{2}(:[0-9a-f]{2}){5}$/i; alert( regexp.test('01:32:54:67:89:AB') ); // true alert( regexp.test('0132546789AB') ); // false (no colons) alert( regexp.test('01:32:54:67:89') ); // false (5 numbers, need 6) alert( regexp.test('01:32:54:67:89:ZZ') ) // false (ZZ in the end)

逐项剖析测试结果与正则的对应关系:

输入结果正则依据
01:32:54:67:89:ABtrue6 段两位十六进制数,冒号分隔,大小写混合(AB)被i标志接受
0132546789ABfalse缺少冒号,无法匹配(:[0-9a-f]{2}){5}中要求的冒号结构
01:32:54:67:89false只有 5 段;{5}要求第 2~6 段全部存在,且$锚点要求结尾恰好落在第 6 段之后
01:32:54:67:89:ZZfalseZZ不在[0-9a-f]字符类内(即使有i标志),无法通过匹配

值得强调的是最后一个用例:ZZ会被[0-9a-f]拒绝,同时$锚点也会拒绝任何「匹配之后还有多余字符」的情况。二者共同保证了输入要么是标准 MAC 地址、要么被整体拒绝。

知识点纵深:为什么这样写是「教科书级」的写法

1. 捕获组 vs 直接重复:(:...){5}背后的设计取舍

有人可能疑惑:既然每段都是[0-9a-f]{2},为什么不直接写[0-9a-f]{2}(:[0-9a-f]{2})重复六次,或者干脆用{6}?关键在于首段与后续段的语法结构不同——首段没有前置冒号,后续 5 段各有前置冒号。因此模式被拆成「首段 + 5 次(冒号+段)」,用捕获组承载「冒号 + 十六进制对」这个重复单元,是最简洁且无歧义的表达。这正是捕获组的核心用途之一:让量词应用于一组字符的复合结构。

2. 捕获组在结果数组中的位置

虽然本例只用test做布尔判断,但要知道(...)是捕获组:若改用str.match(regexp)且不带g标志,匹配结果数组中result[0]是完整匹配,result[1]是最后一个(:[0-9a-f]{2})组的内容(即第 6 段)。组编号从左到右按左括号计数,这一机制在 11-regexp-groups/article.md 的 "Parentheses contents in the match" 与 "Nested groups" 小节有完整演示。

3. 若不想让括号「捕获」:(?:...)非捕获组

如果后续需要match返回更干净的结果、不想让冒号组占用数组下标,可以把捕获组改为非捕获组(?:...):

let regexp = /^[0-9a-f]{2}(?::[0-9a-f]{2}){5}$/i;

(?:...)仍能对量词进行分组,但不计入捕获编号、不产生独立数组条目,也不能在replace中通过$n引用(参见 11-regexp-groups/article.md 的 "Non-capturing groups with ?:" 一节)。在同章习题 04-parse-expression/solution.md 中,正是用(?:\.\d+)?来「保留分组、剔除捕获」。

4. 锚点校验 vs 边界\b查找:两种场景的分工

注意区分两类需求:

  • 整串校验(本任务):用^...$,判断字符串整体是否符合格式;
  • 文本内查找:不加锚点,必要时在尾部加词边界\b防止截断。

同章习题 02-find-webcolor-3-or-6/solution.md 展示了一个经典反例:用/#([a-f0-9]{3}){1,2}/gi在color: #3f3; background-color: #AA00ef; and: #abcd中查找颜色值时,会误匹配#abc(#abcd的前缀);补上\b后/#([a-f0-9]{3}){1,2}\b/gi才得到正确结果。这从侧面印证:「校验整串」与「在文本中查找」使用不同的边界策略,MAC 校验属于前者,必须用锚点而非\b。

实战延伸:从题解走向可复用工具函数

将官方题解封装为可复用的校验函数,并处理空值输入,是生产环境的常见做法:

const MAC_RE = /^[0-9a-f]{2}(:[0-9a-f]{2}){5}$/i; function isMacAddress(value) { return typeof value === 'string' && MAC_RE.test(value); } console.log(isMacAddress('01:32:54:67:89:AB')); // true console.log(isMacAddress('01:32:54:67:89')); // false console.log(isMacAddress(null)); // false

如需支持更多分隔形式(如连字符-、点分格式),可在不破坏整体结构的前提下扩展字符类,例如^[0-9a-f]{2}([-:][0-9a-f]{2}){5}$/i,思路与本解法完全一致:先锁定最小重复单元,再用量词与锚点约束数量和边界。

深入阅读路径

围绕本任务,可在当前仓库中继续研读以下资料:

  • 本章核心概念:11-regexp-groups/article.md(捕获组、嵌套组、可选组、matchAll、命名组(?<name>...)、替换中的$n与$<name>、非捕获组(?:...));
  • 锚点专题:04-regexp-anchors/article.md(^、$与整串匹配测试);
  • 同章对比习题:02-find-webcolor-3-or-6/solution.md(量词作用于组的查找场景)、04-parse-expression/solution.md(非捕获组与命名组的工程化用法);
  • 正则基础:01-regexp-introduction/article.md(两种字面量语法与全部 6 个标志的含义)。

总结

校验 MAC 地址的正则^[0-9a-f]{2}(:[0-9a-f]{2}){5}$/i由三个独立的知识点拼装而成:字符类[0-9a-f]配合i标志定义十六进制字节;捕获组(:[0-9a-f]{2})配合量词{5}精确描述「冒号 + 十六进制对」的 5 次重复;锚点^...$把匹配范围锁定为整个字符串。三者各司其职,缺一不可——这也是所有「格式校验型」正则的通用设计范式:先定义原子单元,再借助分组与量词表达重复结构,最后用锚点约束整体边界。

  • 文档/教程
  • 前端

【免费下载链接】en.javascript.info

Modern JavaScript Tutorial

项目地址:https://gitcode.com/gh_mirrors/en/en.javascript.info
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询