在前端开发中,正则表达式(Regular Expression)一直是一个让人又爱又恨的话题。爱它,是因为它在字符串处理上拥有极其强大的能力;恨它,是因为它的语法看起来像“天书”。
今天,我们将结合拼多多的一道经典笔试题,从正则基础语法出发,一步步推导,最终手写一个前端框架(如 Vue)底层的简易模板渲染引擎。
一、 永远不要相信用户的输入
在真实的业务场景中, “永远不要相信用户的输入” 是一条铁律。我们需要把用户当成“小白”,在数据进入系统前进行严格的校验。
以手机号验证为例,一个合法的中国大陆手机号需要满足以下规则:
- 必须是 11 位数字。
- 必须以
1开头。 - 第二位不能是
0、1、2(即范围在3-9之间)。 - 后面跟着 9 位任意数字。
如果我们将这些规则翻译成正则表达式的语言,就是:
javascript
编辑
1const reg = /^1[3-9]\d{9}$/;
深度解析:
^:匹配字符串的开头,防止前面出现非法字符。1:严格匹配字符1。[3-9]:字符范围,匹配 3 到 9 之间的任意一个数字。\d{9}:\d代表数字,{9}表示前面的字符必须连续出现 9 次。$:匹配字符串的结尾,防止后面出现非法字符。
️ 避坑指南:如果不加
^和$,正则/1[3-9]\d{9}/会匹配到"abc13800000000def"中的手机号,这在表单校验中是致命的错误!
二、 正则表达式的核心语法速查
正则表达式本质上是一个 “字符串筛子” ,它通过模式识别来匹配字符串。以下是几个最核心的基础语法:
/ /:正则表达式的字面量写法,每次匹配一个字符。[]:匹配括号内的字符范围(如[a-z]、[3-9])。{}:表示匹配的次数(量词,如{9}表示匹配 9 次)。\d:预定义字符类,匹配任意数字(等价于[0-9])。^和$:分别表示字符串的开头和结尾,用于精准匹配。
三、 字符串提取与全局匹配
在实际业务中,我们经常需要从一段复杂的文本中提取特定内容。例如,从一段商品描述中提取所有的价格:
javascript
编辑
1const str = '价格是100元,进价是80元,赚了20元';
2const reg = /\d+/g;
3const res = str.match(reg);
4console.log(res); // ["100", "80", "20"]
核心知识点:
\d+:+是量词,表示匹配一次或多次,确保能把连续的数字(如100)完整提取出来。g修饰符:代表全局匹配(Global)。如果不加g,match只会返回第一个匹配到的数字;加上g后,它会像雷达一样扫过整个字符串,不停下,直到提取出所有结果。
四、 字符串替换与分组捕获
正则不仅能“找”,还能“改”。结合 replace 方法和分组捕获 () ,我们可以实现非常优雅的字符串格式化。
场景: 将蛇形命名(如 hello-world)转换为驼峰命名(helloWorld)。
javascript
编辑
1const str = 'hello-world';
2const reg = /-(\w)/g; // 匹配连字符及其后的一个单词字符
3
4const res = str.replace(reg, (_, c) => {
5 // 第一个参数 _ 是完整匹配项(如 "-w")
6 // 第二个参数 c 是分组捕获到的内容(如 "w")
7 return c.toUpperCase();
8});
9console.log(res); // "helloWorld"
核心知识点:
():分组的作用是不改变整体匹配范围,但可以把括号内的内容单独“提取”出来,作为回调函数的参数传递。replace的回调函数:接收匹配到的内容,返回新的字符串进行替换。
五、 进阶实战:手写简易模板引擎
学完了正则,我们来做一个前端面试的高频手写题:实现一个简易的模板渲染函数。
需求: 将模板字符串中的 {{key}} 替换为对象中对应的值。
方案一:早期“笨办法”(无 g + 递归)
javascript
编辑
1function render_v1(template, data) {
2 // 1. 注意:这里没有加全局修饰符 g,每次只找第一个
3 const reg = /{{(\w+)}}/;
4
5 // 2. 只要字符串里还有 {{}},就一直执行
6 if (reg.test(template)) {
7 const key = reg.exec(template)[1]; // 提取变量名,如 "name"
8 template = template.replace(reg, data[key]); // 替换第一个匹配项
9 return render_v1(template, data); // 【核心】递归调用自己,处理下一个
10 }
11
12 return template; // 没有占位符了,返回最终结果
13}
方案二:现代标准写法(全局 g + 回调函数)
javascript
编辑
1function render_v2(template, data) {
2 // 1. 加上 g 表示全局匹配
3 const reg = /{{(\w+)}}/g;
4
5 // 2. 使用 replace 进行全局替换
6 return template.replace(reg, function(match, key) {
7 // match: 完整匹配项 "{{name}}"
8 // key: 捕获组内容 "name"
9 let value = data[key];
10 return value !== undefined ? value : ''; // 容错处理
11 });
12}
💡 深度对比:为什么现代开发推荐方案二?
表格
| 对比维度 | 方案一:递归写法 | 方案二:全局g写法 |
|---|---|---|
| 工作方式 | 剥洋葱式:每次只替换第一个,然后函数重新执行,从头再找一遍。 | 流水线式:正则引擎在底层一次性扫完整个字符串,逐个触发回调。 |
| 性能表现 | 较差:如果有 100 个变量,函数就会重新执行 100 次,产生巨大的内存开销(函数执行上下文压栈)。 | 极高:不管有多少个变量,正则只扫描一遍字符串,没有递归压栈的开销。 |
| 代码可读性 | 逻辑较绕,需要理解递归终止条件。 | 逻辑清晰,符合现代 JS 的声明式编程思维。 |
总结:
以前我们用“递归”,是因为对正则的 replace 回调机制不够熟悉,只能靠“笨办法”一次次重新查找。现在有了全局匹配 g,我们直接让正则引擎在底层一次性扫完。g 修饰符配合 replace 的回调函数,就是用来“杀死”低效递归的终极武器!
六、 总结
从验证手机号的严谨,到全局提取数字的便捷,再到手写模板引擎的递归思想,正则表达式贯穿了前端开发的方方面面。
学习建议:
正则表达式不需要死记硬背,它的本质是模式识别。多动手敲代码,多尝试不同的字符串,观察匹配结果,你很快就能从“面向搜索引擎写正则”进阶为“正则大师”!