写在前面的
前端面试中,正则表达式是一道绕不过去的坎。本文以一道拼多多笔试题为引子——“如何验证用户输入的手机号是否正确?”,把 JS 中正则的核心知识点串起来,从最基础的字符匹配讲到模板引擎的简单实现。
永远不要相信用户的输入——把用户当小白,做好防御性校验。
一、面试题拆解:验证手机号
题目要求很明确,手机号校验规则如下:
我们先把正则写出来,再一步步解释背后的语法:
let reg = /^1[3-9]\\d{9}$/;
console.log(reg.test('13800000090')); // true
看起来只有短短一行,但它包含了正则表达式中最核心的几个概念。下面我们逐个拆解。
二、正则表达式是什么?
你可以把正则表达式理解成一种"模式识别"的语言——就像你给搜索引擎描述一个东西长什么样,它帮你在一堆文字里把符合描述的统统找出来。
打个比方:正则就像一个筛子,你定义好筛孔的形状(模式),把文本往里一倒,符合条件的留下,不符合的漏掉。

JS 中正则的写法是夹在两个 / 之间:
/模式/
每次默认匹配一个字符,想要匹配更多?配合后面讲到的量词就行。
三、核心语法拆解
3.1 [] —— 字符类:划定范围
[] 表示匹配方括号内任意一个字符。注意,是一个,不是多个。
/[abc]/ // 匹配 a、b、c 中的任意一个字符
/[0-9]/ // 匹配 0 到 9 之间的任意一个数字(- 在 [] 内表示范围)
/[3-9]/ // 匹配 3 到 9 之间的任意一个数字
比喻:[] 就像一个单选题的选项集合——A、B、C、D 里你只能选一个。
3.2 \\d —— 预定义字符类:数字快捷方式
\\d 是 [0-9] 的简写,匹配任意一个数字。
/\\d/ // 等价于 /[0-9]/
同理还有:
- \\w:匹配字母、数字、下划线([a-zA-Z0-9_])
- \\s:匹配空白字符(空格、制表符、换行等)
- \\D、\\W、\\S:上面三个的反义(大写 = 取反)
3.3 {} —— 量词:重复次数
{n} 表示前面的字符恰好出现 n 次。
/\\d{9}/ // 匹配 9 个连续数字
/[a-z]{3}/ // 匹配 3 个连续小写字母
其他常见量词:
| {n} | 恰好 n 次 |
| {n,} | 至少 n 次 |
| {n,m} | n 到 m 次 |
| + | 至少 1 次,等价于 {1,} |
| * | 0 次或多次,等价于 {0,} |
| ? | 0 次或 1 次,等价于 {0,1} |
3.4 ^ 和 $ —— 锚点:定死边界
这是最容易忽略但面试常考的点。
- ^:匹配字符串的开头
- $:匹配字符串的结尾
/1[3-9]\\d{9}/ // 匹配 "abc13800000090xyz" 也行,只要**包含**就行
/^1[3-9]\\d{9}$/ // 必须**整个字符串**完全匹配,不能多也不能少
比喻:不加 ^$ 相当于"菜里有肉就行";加了 ^$ 相当于"整盘菜只能是肉,不能有其他东西"。
四、回到手机号正则
现在再看这行正则,应该一眼就看懂了:
/^1[3-9]\\d{9}$/
| ^ | 从字符串开头开始 |
| 1 | 第一位必须是 1 |
| [3-9] | 第二位是 3~9 之间的任意数字 |
| \\d{9} | 后面跟 9 位数字 |
| $ | 到字符串结尾结束 |
拼起来:以 1 开头,第二位 3~9,后面 9 位数字,总共 11 位,完美。
补充:typeof 的小彩蛋
你可能不知道,正则表达式在 JS 中是什么类型?
console.log(typeof /abc/); // "object"
console.log(Object.prototype.toString.call(/abc/)); // "[object RegExp]"
正则本质上是一个对象,类型是 RegExp。所以它才有 .test()、.exec() 这些方法。
五、实战一:提取字符串中的数字
场景:从一段描述文本中提取所有金额。
const str = '价格是100元,进价是80元,赚了20元';
const reg = /\\d+/g;
console.log(str.match(reg)); // ["100", "80", "20"]
这里引入了两个新概念:
5.1 + 量词:匹配一次或多次
\\d+ 表示匹配至少一个连续数字。对比一下:
'abc123def'.match(/\\d/); // ["1"] —— 只匹配一个数字
'abc123def'.match(/\\d+/); // ["123"] —— 尽可能多地匹配连续数字
+ 是贪婪的——在满足条件的前提下,能吞多少吞多少,像一个贪吃蛇。
5.2 g 修饰符:全局匹配
不加 g,match 匹配到第一个就收工;加了 g,它会把整个字符串扫完,把所有匹配结果放进数组返回。
'价格100,进价80'.match(/\\d+/); // ["100"] —— 第一次就停了
'价格100,进价80'.match(/\\d+/g); // ["100", "80"] —— 全部找出来
比喻:不加 g 是"找到第一个就下班";加了 g 是"地毯式搜索,一个不漏"。
六、实战二:replace + 分组 —— 烤串转驼峰
场景:CSS 属性名 background-color 转成 JS 的驼峰命名 backgroundColor。
先看一个常见的写法,再慢慢拆:
const res = "hello-world".replace(/-(\\w)/, (_, c) => {
return c.toUpperCase();
});
console.log(res); // "helloWorld"
这里涉及两个核心机制:捕获分组和replace 的回调。
6.1 () —— 捕获分组
正则中的 () 有两个作用:
const reg = /-(\\w)/;
const result = "hello-world".match(reg);
console.log(result);
// ["-w", "w"]
// ↑ ↑
// 整体 第一个括号捕获的
match 在不加 g 时返回一个数组,第一项是完整匹配,后面依次是每个括号捕获到的内容。
6.2 replace 的回调函数
str.replace(reg, callback) —— 每次匹配成功,回调函数就被调用一次:
- 第一个参数:完整匹配的字符串(这里是 -w)
- 后续参数:依次是每个捕获分组的内容(这里是 w)
"hello-world".replace(/-(\\w)/, (fullMatch, captured) => {
console.log(fullMatch); // "-w"
console.log(captured); // "w"
return captured.toUpperCase(); // "W"
});
// 结果:"helloWorld"
整个流程:找到 -w → 捕获到 w → 把 w 转大写 → 用 W 替换 -w。
6.3 一步到位的写法
利用 $1 这个内置变量(代表第一个捕获分组),可以更简洁:
"hello-world".replace(/-(\\w)/g, '$1'.toUpperCase());
七、实战三:手写一个迷你模板引擎

面试高阶题:给你一个模板字符串和一组数据,实现变量替换。
let template = `我是{{name}},年龄{{age}},性别{{sex}}`;
let person = {
name: "张三",
age: 18,
sex: "男"
};
期望输出:"我是张三,年龄18,性别男"
用正则可以优雅地解决:
function render(template, data) {
const reg = /\\{\\{(\\w+)\\}\\}/g;
// 只要模板里还能找到 {{xxx}},就一直替换
while (reg.test(template)) {
const key = reg.exec(template)[1]; // 提取变量名
template = template.replace(reg, data[key]);
}
return template;
}
console.log(render(template, person));
// "我是张三,年龄18,性别男"
逐行拆解
第一步:正则 /\\{\\{(\\w+)\\}\\}/g
- \\{\\{:匹配两个花括号(花括号在正则中是特殊字符,需要 \\ 转义)
- (\\w+):捕获分组,匹配一个或多个字母/数字/下划线,即变量名
- \\}\\}:匹配两个闭合花括号
- g:全局模式,因为模板里可能有多个 {{xxx}}
第二步:reg.test(template) —— 检查还有没有待替换的占位符
第三步:reg.exec(template)[1] —— 获取第一个捕获分组的内容,即变量名(如 name)
注意:带 g 修饰符的正则在 exec 时会记住上次匹配的位置(通过 lastIndex),所以每次 exec 会匹配下一个占位符。这里用 test 配合 exec 逐个处理。
第四步:template.replace(reg, data[key]) —— 替换当前占位符为实际值
第五步:递归,直到没有占位符为止。
递归版本的问题和改进
上面的递归版本有个小问题:每次 replace 后用同一个 reg 可能因为 lastIndex 状态导致 bug。更稳健的写法:
function render(template, data) {
return template.replace(/\\{\\{(\\w+)\\}\\}/g, (match, key) => {
return data[key] !== undefined ? data[key] : match;
});
}
一次性匹配所有占位符,通过回调函数逐个替换。简洁、安全、高效。
比喻:模板引擎就像填空题——模板是题目(我是___),数据是答案(name: "张三"),正则负责找到所有空位,并填上正确答案。
八、核心方法速查表
| reg.test(str) | 检查是否匹配 | true / false |
| str.match(reg) | 提取匹配内容 | 数组(无 g 也包含分组信息)或 null |
| str.replace(reg, newStr/cb) | 替换匹配内容 | 新字符串 |
| reg.exec(str) | 逐次匹配(配合 g) | 数组(含分组信息)或 null |
九、修饰符一览
| g | global,全局匹配,找到所有 |
| i | ignoreCase,忽略大小写 |
| m | multiline,多行模式,^$ 匹配每行的开头结尾 |
十、总结
本文从一道拼多多面试题出发,串联起了 JS 正则的核心知识点:
总结正则的要义:模式描述 + 量词控制 + 分组提取。掌握了这三板斧,日常开发中 80% 的正则场景都能从容应对。


