正则表达式入门到进阶:从表单校验到手写模板引擎

写给前端工程师的正则教程:不背符号表,先理解"正则引擎怎么找东西"。

1. 正则到底是什么?先把它想成"文本侦探"

日常开发里,我们每天都在和字符串打交道:

  • 用户输入手机号、邮箱、身份证号;
  • 后端返回一段日志,需要从里面提取错误码;
  • 页面模板里写了 {{name}},需要替换成真实数据;
  • 文件名、路由、URL 参数、金额格式都要做校验和转换。

如果只用 ifforsplitincludes 去处理这些问题,很快就会陷入一堆分支判断。正则表达式(Regular Expression)就是为了解决这类"文本模式匹配"问题而生的工具。

你可以把正则想象成一个文本侦探。它拿着一张"通缉令",在字符串街区里从左往右巡逻:

  • \d:我要找数字;
  • [a-z]:我要找小写字母;
  • +:前面的东西至少出现一次,越多越好;
  • ^:必须从街区门口开始;
  • $:必须刚好走到街区尽头;
  • ():把抓到的关键人单独带回审讯室,也就是捕获分组。

正则不是魔法,而是一套字符串扫描规则。理解这一点,后面所有符号都会变得合理。

2. 第一准则:永远不要相信用户输入

任何面向用户的系统,都应该默认用户输入是不可靠的。

用户可能在手机号输入框里输入:

txt 复制代码
hello
13888888888 
<script>alert(1)</script>
100元

这些内容如果不处理就发给后端,轻则接口报错,重则引发安全风险。前端校验不能代替后端校验,但它是第一道体验防线:能提前拦住明显错误,减少无效请求,也能给用户更及时的反馈。

例如手机号校验,最常见的需求是:

  1. 总长度为 11 位;
  2. 第一位是 1
  3. 第二位通常是 3-9
  4. 后面 9 位都是数字。

对应正则:

js 复制代码
const phoneReg = /^1[3-9]\d{9}$/;

console.log(phoneReg.test('13888888888')); // true
console.log(phoneReg.test('12888888888')); // false
console.log(phoneReg.test('13888888888 ')); // false

这里最关键的是 ^$。很多初学者写成:

js 复制代码
/1[3-9]\d{9}/

这个写法看似也能匹配手机号,但它只是说"字符串里出现过一段像手机号的内容"就行。比如:

js 复制代码
console.log(/1[3-9]\d{9}/.test('abc13888888888xyz')); // true

这显然不是严格校验。严格校验必须加上边界锚点:

js 复制代码
/^1[3-9]\d{9}$/

这就像安检,不是"包里有身份证就行",而是"你整个人从头到脚都必须符合规则"。

3. JavaScript 里的正则是什么类型?

在 JavaScript 中,正则是对象,准确说是 RegExp 对象。

js 复制代码
const reg = /\d+/;

console.log(typeof reg); // "object"
console.log(Object.prototype.toString.call(reg)); // "[object RegExp]"

typeof 对引用类型区分能力有限。数组、对象、正则都会被归类为 object。如果想精确判断类型,常用:

js 复制代码
Object.prototype.toString.call(value)

正则对象可以通过两种方式创建。

第一种是字面量:

js 复制代码
const reg = /\d+/g;

第二种是构造函数:

js 复制代码
const reg = new RegExp('\\d+', 'g');

日常开发优先使用字面量,清晰、直观、少写转义。只有当正则规则需要动态拼接时,才更适合使用 new RegExp()

js 复制代码
const keyword = 'vue';
const reg = new RegExp(keyword, 'i');

console.log(reg.test('Vue Router')); // true

4. 正则基础语法:先记"动作",再记符号

很多人学正则痛苦,是因为一上来就背符号。更好的方式是先问:我想让引擎做什么动作?

4.1 匹配某类字符

js 复制代码
\d    // 数字,等价于 [0-9]
\D    // 非数字
\w    // 字母、数字、下划线,等价于 [A-Za-z0-9_]
\W    // 非 \w
\s    // 空白字符,如空格、换行、制表符
\S    // 非空白字符
.     // 除换行外的任意单个字符

示例:

js 复制代码
const text = '订单 A1024 金额 99 元';

console.log(text.match(/\d+/g)); // ['1024', '99']
console.log(text.match(/\w+/g)); // ['A1024', '99']

注意:\w 只覆盖英文、数字、下划线,不包含中文。如果你想匹配中文,常见写法是:

js 复制代码
const chineseReg = /[\u4e00-\u9fa5]+/g;

4.2 自定义字符集合

字符集合用 []

js 复制代码
[abc]     // a、b、c 中任意一个
[0-9]     // 任意数字
[a-z]     // 任意小写字母
[^0-9]    // 非数字

手机号第二位为什么写 [3-9]

js 复制代码
/^1[3-9]\d{9}$/

因为第二位只能从 39 之间选一个,而不是随便一个数字。

4.3 控制出现次数

量词决定"前面的规则出现多少次"。

js 复制代码
*      // 0 次或多次
+      // 1 次或多次
?      // 0 次或 1 次
{n}    // 恰好 n 次
{n,}   // 至少 n 次
{n,m}  // n 到 m 次

示例:

js 复制代码
console.log('abc123'.match(/\d+/)); // ['123']
console.log('abc'.match(/\d*/));    // [''],因为 * 允许 0 次
console.log('aaa'.match(/a{2}/));   // ['aa']

初学者最容易被 * 坑到,因为它可以匹配空字符串。做校验时,如果业务要求必须有内容,通常更应该考虑 +

4.4 控制位置

位置锚点不匹配具体字符,它匹配"位置"。

js 复制代码
^    // 字符串开头
$    // 字符串结尾
\b   // 单词边界
\B   // 非单词边界

严格校验邮箱、手机号、身份证号时,通常都需要 ^$。否则正则只要在字符串中间找到一段符合规则的内容,就会返回成功。

js 复制代码
const reg = /^\d{6}$/;

console.log(reg.test('123456'));     // true
console.log(reg.test('xx123456yy')); // false

5. 正则引擎怎么工作?它不是一次看完整个字符串

正则引擎大多数时候是从左到右扫描字符串。它不会一眼看完整个文本,而是像拿着手电筒在走廊里移动:

  1. 从当前位置开始尝试匹配;
  2. 当前字符不符合,就移动到下一个位置;
  3. 符合后继续匹配后续规则;
  4. 如果后续规则失败,可能会回退,也就是回溯;
  5. 找到结果后,根据是否有 g 决定继续扫描还是停止。

看一个例子:

js 复制代码
const str = '价格是100元,进价是80,赚了20';
const reg = /\d+/g;

console.log(str.match(reg)); // ['100', '80', '20']

执行过程可以理解为:

  • 扫描到"价格是",不是数字,跳过;
  • 遇到 1,符合 \d
  • 因为后面有 +,继续吃掉 00
  • 遇到"元",数字中断,得到 100
  • 因为有 g,继续往后扫描;
  • 最终得到 8020

这就是 + 的"贪婪"特性:只要还能匹配,它就尽可能多拿一点。

6. 贪婪、惰性和回溯:正则里的"我全都要"和"我先少拿点"

默认情况下,量词是贪婪的。

js 复制代码
const html = '<span>hello</span><span>world</span>';

console.log(html.match(/<span>.*<\/span>/)[0]);
// <span>hello</span><span>world</span>

.* 会尽可能多地匹配,因此它从第一个 <span> 一路吃到最后一个 </span>

如果只想匹配第一个标签内容,可以使用惰性量词:

js 复制代码
console.log(html.match(/<span>.*?<\/span>/)[0]);
// <span>hello</span>

*?+???{n,m}? 都是惰性写法。惰性不是"不匹配",而是"先尽量少匹配,如果后续规则不成立,再一点点扩张"。

回溯则是正则引擎为了让整体匹配成功而做的"后悔一步"。

js 复制代码
const str = 'aaab';
console.log(/a+b/.test(str)); // true

a+ 会先吃掉三个 a,然后 b 匹配最后一个字符成功,不需要回溯。如果字符串是 aaac,引擎会尝试回退 a+ 吃掉的内容,希望后面的 b 能成功,但怎么退都找不到 b,最后失败。

小规模回溯很正常。可怕的是灾难性回溯。

js 复制代码
/^(a+)+$/

面对一长串 aaaaaaaaaaaaab,这种嵌套量词可能让引擎尝试大量组合,造成性能问题,甚至形成 ReDoS(Regular Expression Denial of Service,正则拒绝服务)风险。

工程建议:

  • 避免在用户可控长文本上使用复杂嵌套量词;
  • 能写明确范围就不要写过度宽泛的 .*
  • 对输入长度做限制;
  • 对高风险正则做性能测试;
  • 服务端不要随便执行用户提交的正则。

7. 分组:把抓到的内容装进口袋

括号 () 有两个作用:

  1. 改变优先级;
  2. 捕获内容。

7.1 捕获分组

js 复制代码
const date = '2026-06-18';
const reg = /^(\d{4})-(\d{2})-(\d{2})$/;

const result = date.match(reg);

console.log(result[0]); // 2026-06-18
console.log(result[1]); // 2026
console.log(result[2]); // 06
console.log(result[3]); // 18

result[0] 是完整匹配,result[1] 开始才是括号捕获到的内容。

7.2 命名分组

现代 JavaScript 支持命名捕获分组:

js 复制代码
const reg = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/;
const result = '2026-06-18'.match(reg);

console.log(result.groups.year);  // 2026
console.log(result.groups.month); // 06
console.log(result.groups.day);   // 18

命名分组让代码更可读,特别适合解析日期、URL、日志。

7.3 非捕获分组

如果你只想分组,不想保存捕获结果,可以写:

js 复制代码
(?:abc)

例如:

js 复制代码
const reg = /^(?:https?:\/\/)?example\.com$/;

这里 (?:https?:\/\/)? 只是为了把协议部分整体设为可选,不需要把它捕获出来。

7.4 反向引用

反向引用可以匹配"之前捕获过的同样内容"。

js 复制代码
const reg = /\b(\w+)\s+\1\b/;

console.log(reg.test('hello hello')); // true
console.log(reg.test('hello world')); // false

这里 \1 表示第一个捕获分组的内容。它常用于检测重复词、成对标签等场景。

8. 常用修饰符:正则的模式开关

JavaScript 常见修饰符:

js 复制代码
g  // global,全局匹配
i  // ignoreCase,忽略大小写
m  // multiline,多行模式,影响 ^ 和 $
s  // dotAll,让 . 可以匹配换行符
u  // unicode,按 Unicode 语义处理
y  // sticky,粘连匹配,从 lastIndex 精确位置开始
d  // indices,返回匹配索引信息

常用组合:

js 复制代码
/hello/i      // 忽略大小写
/\d+/g        // 提取所有连续数字
/^title/gm    // 多行中匹配每一行开头的 title
/[\s\S]*/     // 兼容匹配任意字符,包括换行
/.*?/s        // dotAll 模式下,. 也能跨行

注意一个很容易面试被问到的坑:带 g 的正则调用 test() 会记录 lastIndex

js 复制代码
const reg = /\d/g;

console.log(reg.test('1')); // true
console.log(reg.test('1')); // false
console.log(reg.test('1')); // true

为什么第二次是 false?因为第一次匹配后,lastIndex 移到了 1,第二次从索引 1 开始找,已经到末尾了。日常做表单校验时,不建议给 .test() 使用的正则加 g

js 复制代码
const safeReg = /^\d$/;

这比 /^\d$/g 稳定。

9. 字符串 API 实战:正则不是单兵作战

正则真正强大,是因为它能和 JavaScript 字符串 API 配合。

9.1 test:只问是否匹配

js 复制代码
const reg = /^1[3-9]\d{9}$/;

if (!reg.test(phone)) {
  console.log('手机号格式不正确');
}

.test() 返回布尔值,适合校验。

9.2 match:提取匹配结果

js 复制代码
const text = '苹果 12 元,香蕉 8 元,西瓜 25 元';

console.log(text.match(/\d+/g));
// ['12', '8', '25']

不带 g 时,match() 会返回更详细的信息:

js 复制代码
const result = '2026-06-18'.match(/^(\d{4})-(\d{2})-(\d{2})$/);

console.log(result[0]); // 完整匹配
console.log(result[1]); // 年
console.log(result.index); // 匹配起始位置
console.log(result.input); // 原始字符串

g 时,它返回所有完整匹配项,不返回每个捕获分组的详细信息。如果你既想全局匹配,又想拿分组,优先考虑 matchAll()

9.3 matchAll:全局捕获的优雅写法

js 复制代码
const text = 'name=张三; age=18; city=杭州';
const reg = /(\w+)=([^;]+)/g;

for (const item of text.matchAll(reg)) {
  console.log(item[1], item[2]);
}

输出:

txt 复制代码
name 张三
age 18
city 杭州

matchAll() 返回的是迭代器,适合配合 for...ofArray.from()

9.4 exec:正则对象自己的扫描器

js 复制代码
const reg = /(\d+)/g;
const text = 'a1 b22 c333';

let match;
while ((match = reg.exec(text)) !== null) {
  console.log(match[0], match.index);
}

exec() 很适合手动控制扫描过程。带 g 时,它会不断更新 lastIndex

9.5 replace:正则进阶的核心战场

replace() 不只是替换固定字符串,它还可以接收函数。

js 复制代码
const str = 'hello-world';

const result = str.replace(/-(\w)/g, (_, letter) => {
  return letter.toUpperCase();
});

console.log(result); // helloWorld

这段代码的含义是:

  1. 找到 -w 这种结构;
  2. (\w)w 捕获出来;
  3. 回调函数返回 W
  4. W 替换掉整个 -w

replace 回调参数顺序大致是:

js 复制代码
replace((match, group1, group2, ..., offset, input, groups) => {})

如果你看到面试题问"replace 第二个参数是函数时会收到什么",核心答案就是:完整匹配、捕获分组、匹配索引、原字符串,以及命名分组对象。

10. 经典需求:把 kebab-case 转成 camelCase

需求:

txt 复制代码
user-name      -> userName
border-left    -> borderLeft
hello-world-js -> helloWorldJs

实现:

js 复制代码
function toCamelCase(str) {
  return str.replace(/-([a-zA-Z])/g, (_, letter) => {
    return letter.toUpperCase();
  });
}

console.log(toCamelCase('hello-world-js')); // helloWorldJs

为什么要加 g?因为一个字符串里可能有多个连字符。

如果不加:

js 复制代码
'hello-world-js'.replace(/-([a-zA-Z])/, (_, s) => s.toUpperCase());
// helloWorld-js

只替换了第一处。

11. 终极实战:手写一个微型模板引擎

现在进入最有意思的地方。很多模板语法长这样:

txt 复制代码
我是 {{name}},今年 {{age}} 岁,来自 {{city}}

我们希望用数据对象替换占位符:

js 复制代码
const data = {
  name: '张三',
  age: 18,
  city: '杭州'
};

最小实现:

js 复制代码
function render(template, data) {
  return template.replace(/{{\s*(\w+)\s*}}/g, (_, key) => {
    return data[key] ?? '';
  });
}

const template = '我是 {{name}},今年 {{age}} 岁,来自 {{city}}';

console.log(render(template, data));
// 我是 张三,今年 18 岁,来自 杭州

这里有几个关键点:

  • {{}} 是普通字符,但 {} 在正则里有特殊含义,所以写法要谨慎;
  • \s* 允许占位符内有空格;
  • (\w+) 捕获变量名;
  • g 表示替换所有占位符;
  • 回调函数根据变量名去 data 里取值。

如果想支持点路径:

txt 复制代码
{{user.name}}
{{user.profile.city}}

可以写:

js 复制代码
function getValueByPath(data, path) {
  return path.split('.').reduce((obj, key) => {
    return obj == null ? undefined : obj[key];
  }, data);
}

function render(template, data) {
  return template.replace(/{{\s*([\w.]+)\s*}}/g, (_, path) => {
    const value = getValueByPath(data, path);
    return value == null ? '' : String(value);
  });
}

const template = '用户:{{user.name}},城市:{{user.profile.city}}';
const data = {
  user: {
    name: '小明',
    profile: {
      city: '深圳'
    }
  }
};

console.log(render(template, data));
// 用户:小明,城市:深圳

这就是模板引擎的核心雏形。真实框架当然复杂得多,还会涉及 AST、依赖收集、编译优化、XSS 防护等问题,但"扫描模板 → 捕获变量 → 替换内容"这条主线是一样的。

安全提醒:如果模板渲染结果会插入 HTML,不要直接信任用户数据。需要做 HTML 转义,否则可能引发 XSS。

js 复制代码
function escapeHTML(str) {
  return String(str)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

12. 常见业务正则速查

下面这些写法可以作为学习材料。真实业务中要根据产品规则调整,不要盲目复制。

12.1 去除首尾空格

js 复制代码
function trim(str) {
  return str.replace(/^\s+|\s+$/g, '');
}

12.2 提取 URL 参数

js 复制代码
function parseQuery(url) {
  const query = url.split('?')[1] || '';
  const result = {};

  query.replace(/([^&=]+)=([^&]*)/g, (_, key, value) => {
    result[decodeURIComponent(key)] = decodeURIComponent(value);
    return '';
  });

  return result;
}

console.log(parseQuery('https://a.com?a=1&name=%E5%BC%A0%E4%B8%89'));

更推荐工程中使用 URLSearchParams,但面试中手写解析能体现正则和字符串处理能力。

12.3 手机号脱敏

js 复制代码
function maskPhone(phone) {
  return phone.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2');
}

console.log(maskPhone('13888889999')); // 138****9999

12.4 金额千分位

js 复制代码
function formatMoney(num) {
  const [integer, decimal] = String(num).split('.');
  const formatted = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  return decimal ? `${formatted}.${decimal}` : formatted;
}

console.log(formatMoney(1234567.89)); // 1,234,567.89

这里的 (?=...) 是正向先行断言,它只检查后面是否符合条件,不消费字符。\B 表示非单词边界。这个题是大厂面试高频题。

12.5 密码强度校验

要求:至少 8 位,必须包含字母和数字。

js 复制代码
const passwordReg = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/;

解释:

  • (?=.*[A-Za-z]):后面必须能找到一个字母;
  • (?=.*\d):后面必须能找到一个数字;
  • [A-Za-z\d]{8,}:整体只能由字母数字组成,至少 8 位。

如果还要求特殊符号,可以继续加先行断言。

13. 进阶语法:断言像"门口保安",看一眼但不带走

断言不会消费字符,只判断某个位置是否满足条件。

13.1 正向先行断言

js 复制代码
const reg = /\d+(?=元)/g;

console.log('苹果10元,香蕉8元'.match(reg)); // ['10', '8']

\d+(?=元) 的意思是:匹配数字,但要求数字后面跟着"元"。最终结果不包含"元"。

13.2 负向先行断言

js 复制代码
const reg = /\d+(?!元)/g;

console.log('苹果10元,库存20个'.match(reg)); // ['2', '0'] 或受扫描细节影响

负向断言要谨慎使用,因为它判断的是当前位置后面"不是什么",复杂场景容易产生意外结果。更好的做法通常是先明确上下文,再提取。

13.3 后行断言

js 复制代码
const reg = /(?<=¥)\d+/g;

console.log('价格¥99,优惠¥20'.match(reg)); // ['99', '20']

(?<=¥) 表示当前位置前面必须是 。注意,后行断言在现代环境中支持较好,但如果要兼容老旧浏览器,需要谨慎。

14. 正则可读性:写给机器,也写给未来的自己

正则很容易变成"天书"。工程里要避免炫技。

不推荐:

js 复制代码
/^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$/

更推荐:

js 复制代码
const emailReg = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

这不是最严格的邮箱标准,但对大多数业务表单足够。真正严格的邮箱规范非常复杂,不建议在前端用一条巨型正则硬扛。

提升可读性的建议:

  • 给正则变量取清晰名字,如 phoneRegtemplateVarReg
  • 为复杂正则写注释;
  • 把复杂规则拆成多个步骤;
  • 有现成标准 API 时优先使用标准 API,比如 URL 解析用 URLURLSearchParams
  • 校验规则和业务文案放在同一处,方便维护。

15. 正则与性能:别让一条规则拖垮页面

正则性能问题主要来自三个地方:

  1. 输入字符串太长;
  2. 正则写得太模糊;
  3. 嵌套量词导致大量回溯。

危险示例:

js 复制代码
/^(a+)+$/

改进思路:

js 复制代码
/^a+$/

如果业务规则本来只是"全是 a",就不需要嵌套。

再看一个常见危险写法:

js 复制代码
/^.*(error).*$/

很多时候可以改成:

js 复制代码
/error/

如果你只是判断是否包含 error,就不要让正则从头吃到尾再回溯。

工程中处理用户输入时,除了正则本身,还要限制输入长度:

js 复制代码
if (value.length > 1000) {
  throw new Error('输入过长');
}

安全不是靠一条正则完成的,而是靠"长度限制 + 类型校验 + 白名单规则 + 后端兜底"一起完成。

16. 大厂面试题精选

下面这些题目覆盖阿里、腾讯、字节、美团、拼多多等前端面试中常见的正则方向。题目不一定原样出现,但考点高度重合。

题 1:严格校验手机号

要求:1 开头,第二位 3-9,总共 11 位。

js 复制代码
const reg = /^1[3-9]\d{9}$/;

考点:锚点、字符集合、量词、严格匹配。

题 2:为什么 /\d/g.test('1') 连续调用结果会变?

js 复制代码
const reg = /\d/g;

console.log(reg.test('1')); // true
console.log(reg.test('1')); // false

答案:带 g 的正则会维护 lastIndex。第一次匹配后 lastIndex 移到末尾,第二次从末尾继续找,自然失败。表单校验不要给 test() 正则加 g

题 3:实现字符串首尾空格 trim

js 复制代码
function trim(str) {
  return str.replace(/^\s+|\s+$/g, '');
}

考点:^$\s、或运算 |

题 4:提取字符串中的所有数字

js 复制代码
const str = '价格100,库存20,优惠5';
console.log(str.match(/\d+/g)); // ['100', '20', '5']

追问:返回值是什么类型?如果没有匹配到呢?

答案:匹配到返回数组;没有匹配到返回 null

题 5:把 hello-world-js 转成 helloWorldJs

js 复制代码
function camelize(str) {
  return str.replace(/-([a-zA-Z])/g, (_, letter) => {
    return letter.toUpperCase();
  });
}

考点:捕获分组、replace 回调、全局替换。

题 6:把手机号中间四位替换为星号

js 复制代码
function maskPhone(phone) {
  return phone.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2');
}

考点:捕获分组、替换占位符 $1$2

题 7:实现千分位格式化

js 复制代码
function format(num) {
  const [int, dec] = String(num).split('.');
  const res = int.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  return dec ? `${res}.${dec}` : res;
}

考点:先行断言、非单词边界、从右向左的分组思维。

题 8:解析模板字符串

js 复制代码
function render(template, data) {
  return template.replace(/{{\s*([\w.]+)\s*}}/g, (_, path) => {
    return path.split('.').reduce((obj, key) => obj?.[key], data) ?? '';
  });
}

考点:模板引擎原理、分组捕获、对象路径读取。

题 9:matchexecmatchAll 有什么区别?

简答:

  • test():只返回布尔值;
  • match():字符串方法,不带 g 返回详细信息,带 g 返回完整匹配数组;
  • exec():正则方法,可配合 g 手动迭代;
  • matchAll():适合全局匹配并保留捕获分组。

题 10:写一个密码校验规则

要求:至少 8 位,包含数字和字母。

js 复制代码
const reg = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/;

考点:正向先行断言。

题 11:判断是否有连续重复单词

js 复制代码
const reg = /\b(\w+)\s+\1\b/;

console.log(reg.test('hello hello')); // true

考点:反向引用。

题 12:提取价格中的数字,但不包含货币符号

js 复制代码
const str = '¥99 ¥128';
console.log(str.match(/(?<=¥)\d+/g)); // ['99', '128']

考点:后行断言。

题 13:为什么不建议用一条超复杂正则校验所有邮箱?

答案:邮箱规范本身复杂,前端业务多数只需要基础格式校验。超复杂正则可读性差、维护成本高,也不一定覆盖所有合法邮箱。工程上常用:

js 复制代码
const emailReg = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

后端再做更严格校验或发送验证邮件。

题 14:什么是灾难性回溯?如何避免?

示例:

js 复制代码
/^(a+)+$/

当输入接近匹配但最终失败时,可能触发大量回溯。避免方式:

  • 不写不必要的嵌套量词;
  • 限制输入长度;
  • 用更明确的字符集合;
  • 将复杂规则拆成多个简单判断。

题 15:如何匹配 HTML 标签?

面试陷阱:不要试图用一条正则完整解析 HTML。HTML 是嵌套结构,正则不适合完整解析。简单提取可以用正则,可靠解析应使用 DOMParser 或 HTML parser。

js 复制代码
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');

这道题考的是边界意识,而不是让你写出一条"神级正则"。

题 16:如何删除字符串中所有非数字字符?

js 复制代码
function onlyNumber(str) {
  return str.replace(/\D/g, '');
}

console.log(onlyNumber('a1b2c3')); // 123

考点:\D 和全局替换。

题 17:如何判断一个字符串是否全是中文?

js 复制代码
const reg = /^[\u4e00-\u9fa5]+$/;

注意:这只是常用中文范围,不覆盖所有 CJK 扩展字符。业务若涉及多语言姓名,应谨慎设计。

题 18:解释 /a.*?b//a.*b/ 的区别

答案:

  • /a.*b/ 是贪婪匹配,会尽可能匹配更长内容;
  • /a.*?b/ 是惰性匹配,会尽可能匹配更短内容,但仍要保证整体成功。

17. 学习路线:从会写到写得稳

建议按这条路线练习:

  1. 先掌握字符类:\d\w\s[]
  2. 再掌握量词:+*?{n,m}
  3. 接着理解位置:^$\b
  4. 然后学习分组:(), (?:), (?<name>)
  5. 再练 replace 回调;
  6. 最后研究断言、回溯、性能和安全。

练习题建议:

  • 写手机号校验;
  • 写邮箱基础校验;
  • 从一段日志里提取时间、等级和错误码;
  • 写 kebab-case 到 camelCase;
  • 写手机号脱敏;
  • 写千分位;
  • 写模板引擎;
  • 找出一条正则的性能风险。

18. 总结

正则表达式的核心不是背符号,而是理解"规则如何驱动引擎移动"。它像一个文本侦探,从左到右扫描字符串,按规则寻找目标;量词决定它拿多少,分组决定它把什么装进口袋,修饰符决定它找一个还是找全部。

在真实工程中,正则最常见的价值有三类:

  • 校验:手机号、邮箱、密码、表单字段;
  • 提取:金额、日期、URL 参数、日志信息;
  • 转换:命名格式转换、模板渲染、数据脱敏。

但正则不是万能钥匙。遇到复杂嵌套结构、HTML 解析、超严格国际化校验时,不要硬写一条巨型正则。专业工程师的能力不只是"能写正则",还包括知道什么时候不用正则。

把正则写得正确,是入门;把正则写得可读、可维护、性能稳定,才是进阶。

相关推荐
阿祖zu1 小时前
别再优化 RAG 了,适配 Agent 的 LLM Wiki 知识库理念
前端·后端·aigc
kyriewen2 小时前
前端错误监控最全指南:捕获 JS 异常、Promise 拒绝、资源加载失败,附上报代码
前端·javascript·监控
狗哥哥2 小时前
船队运营可视化技术方案
前端
大家的林语冰2 小时前
ESLint 近期动态大全,新版本正式发布,antfu 大佬推荐的插件也更新了!
前端·javascript·前端工程化
只会cv的前端攻城狮2 小时前
DSL 领域模型架构设计:消灭 CRUD 重复工作
前端·架构
神奇小汤圆2 小时前
RAG大厂面试题汇总:向量检索、混合检索、Rerank、幻觉处理高频问题
面试
码事漫谈3 小时前
时序数据库2026盘点:国产数据库如何以“融合多模”走出差异化之路?
前端·后端
道友可好3 小时前
让 AI 自己验收,等于让学生自己批卷
前端·人工智能·后端
yingyima3 小时前
Go 语言正则表达式速查手册:30 分钟掌握核心语法与实战技巧
前端