拼多多笔试题复盘:从正则表达式到手写简易模板引擎

1 阅读5分钟

在前端开发中,正则表达式(Regular Expression)一直是一个让人又爱又恨的话题。爱它,是因为它在字符串处理上拥有极其强大的能力;恨它,是因为它的语法看起来像“天书”。

今天,我们将结合拼多多的一道经典笔试题,从正则基础语法出发,一步步推导,最终手写一个前端框架(如 Vue)底层的简易模板渲染引擎。

一、 永远不要相信用户的输入

在真实的业务场景中, “永远不要相信用户的输入” 是一条铁律。我们需要把用户当成“小白”,在数据进入系统前进行严格的校验。

手机号验证为例,一个合法的中国大陆手机号需要满足以下规则:

  1. 必须是 11 位数字。
  2. 必须以 1 开头。
  3. 第二位不能是 012(即范围在 3-9 之间)。
  4. 后面跟着 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)。如果不加 gmatch 只会返回第一个匹配到的数字;加上 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 的回调函数,就是用来“杀死”低效递归的终极武器!

六、 总结

从验证手机号的严谨,到全局提取数字的便捷,再到手写模板引擎的递归思想,正则表达式贯穿了前端开发的方方面面。

学习建议:
正则表达式不需要死记硬背,它的本质是模式识别。多动手敲代码,多尝试不同的字符串,观察匹配结果,你很快就能从“面向搜索引擎写正则”进阶为“正则大师”!