- 文档/教程
- 前端
【免费下载链接】en.javascript.info
Modern JavaScript Tutorial
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 的定义:
模式的一部分可以用括号
(...)括起来,这被称为「捕获组」,有两个作用:
- 允许把匹配的一部分作为独立条目放进结果数组;
- 如果在括号后放置量词,量词将作用于括号整体。
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:AB | true | 6 段两位十六进制数,冒号分隔,大小写混合(AB)被i标志接受 |
0132546789AB | false | 缺少冒号,无法匹配(:[0-9a-f]{2}){5}中要求的冒号结构 |
01:32:54:67:89 | false | 只有 5 段;{5}要求第 2~6 段全部存在,且$锚点要求结尾恰好落在第 6 段之后 |
01:32:54:67:89:ZZ | false | ZZ不在[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
相关推荐
JavaScript 正则表达式中的捕获组详解
JavaScript 正则表达式中的捕获组详解 正则表达式是 JavaScript 中强大的文本处理工具,而捕获组则是正则表达式中一个非常重要的概念。本文将深入
文档教程前端JavaScript 正则表达式中的捕获组详解
JavaScript 正则表达式中的捕获组详解 正则表达式是 JavaScript 中处理字符串的强大工具,而捕获组 capturing groups 则是正则
JavaScript 正则表达式中的捕获组详解
JavaScript 正则表达式中的捕获组详解 正则表达式是 JavaScript 中处理字符串的强大工具,而捕获组 capturing groups 则是正则
文档/教程前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考