正则表达式基础

用一段"暗号"精准匹配你想要的文本

🎯 学习目标

学完本章,你将能够:

  • 能够使用字面量和 RegExp 构造函数创建正则表达式
  • 能够运用字符类(\d、\w、\s 等)和量词(*、+、?、{n,m})描述文本模式
  • 能够使用锚点(^、$、\b)和分组(())精确控制匹配位置
  • 能够使用 String 方法(test、match、replace、split)结合正则完成常见文本处理任务
  • 能够编写正则表达式解决邮箱验证、手机号提取、模板替换等实际问题

你有没有想过

怎么在一大段文字里找出所有手机号?怎么验证用户输入的邮箱格式对不对?怎么把 "2024-01-15" 变成 "01/15/2024"?

如果用字符串方法逐个判断,代码又长又容易漏情况。正则表达式(Regular Expression,简称 regex)就是解决这类问题的利器——用一段简短的模式描述你要匹配的文本规则。

用人话说:正则就是你写给计算机的一句"暗号",告诉它"我要找长这样的字符串"。

Part 1: 语法基础

正则看起来像乱码,但拆开看每个符号都有明确含义

/^\d{3}-\d{4}$/ 这种"外星文"让人头大。但别慌,把它拆成零件:\d 是数字、{3} 是三个、^ 是开头、$ 是结尾。每个符号都有唯一的意思。这节学三类核心语法——字符类匹配"什么",量词匹配"几个",锚点定位"在哪"。拼起来就能描述任何文本规则。

创建正则

两种方式:斜杠字面量和 RegExp 构造函数

// 方式一:字面量(推荐,简洁)
let re1 = /hello/i;

// 方式二:构造函数(适合动态拼接模式)
let pattern = "hello";
let re2 = new RegExp(pattern, "i");

如果模式是固定的,用字面量 /pattern/flags。如果模式需要动态生成(比如来自用户输入),用 new RegExp()。

字符类

用简写匹配某一类字符

写法含义等价于
.任意字符(除换行)—
\d数字[0-9]
\D非数字[^0-9]
\w字母数字下划线[a-zA-Z0-9_]
\s空白字符空格、制表、换行
[abc]字符集匹配 a 或 b 或 c
[^abc]排除字符集除了 a、b、c

大写是小写的"反面":\d 是数字,\D 是非数字。

量词

控制前面的元素出现几次

量词含义示例
*0 次或多次\d* 匹配 ""、"123"
+1 次或多次\d+ 匹配 "123",不匹配 ""
?0 次或 1 次https? 匹配 "http" 或 "https"
{n}恰好 n 次\d{3} 匹配 "123"
{n,}至少 n 次\d{2,} 匹配 "12"、"123"
{n,m}n 到 m 次\d{2,4} 匹配 "12"、"1234"

锚点与修饰符

锚点定位位置,修饰符改变匹配行为

// 锚点——不匹配字符,匹配位置
// ^  字符串开头    $  字符串结尾
// \b 单词边界
console.log(/^\d+$/.test("123"));  // true(全数字)
console.log(/^hello/.test("hello world")); // true(以hello开头)

// 修饰符——写在正则最后
// i  忽略大小写
// g  全局匹配(找所有,不是找第一个就停)
// m  多行模式(^$ 匹配每行开头结尾)
// s  dotAll(. 也能匹配换行符)
// u  Unicode 模式

最常用的组合:/pattern/gi(全局 + 忽略大小写)。

Part 2: 分组与方法

分组让你提取匹配的"片段",方法让你用正则做实际操作

光知道一段文本"匹不匹配"还不够,你还要知道"匹配到的具体内容是什么"。用括号把正则的一部分包起来就成了"分组",可以单独提取。"2024-01-15".match(re) 能把年月日分别拿出来。再配合 test、replace、split 这些方法,正则从"能判断"升级到"能干活"。

分组与引用

括号包起来的是"组",可以单独提取

// 普通分组
let re = /(\d{4})-(\d{2})-(\d{2})/;
let match = "2024-01-15".match(re);
console.log(match[0]); // "2024-01-15"(完整匹配)
console.log(match[1]); // "2024"(第一组)
console.log(match[2]); // "01"(第二组)
console.log(match[3]); // "15"(第三组)

// 命名捕获组(ES2018,推荐)
let re2 = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
let { year, month, day } = re2.exec("2024-01-15").groups;
console.log(year, month, day); // "2024" "01" "15"

命名捕获组给每个分组起了名字,代码可读性大幅提升。

test 与 exec

test 判断有没有,exec 提取匹配详情

// test —— 返回 true/false
console.log(/\d+/.test("abc123")); // true
console.log(/^\d+$/.test("123"));  // true
console.log(/^\d+$/.test("12a"));  // false

// exec —— 返回匹配结果对象(配合 g 修饰符循环)
let re = /(\w+)@(\w+)\.(\w+)/g;
let result;
while ((result = re.exec("a@b.com x@y.org")) !== null) {
    console.log(result[0], result[1], result[2]);
}

字符串正则方法

match、matchAll、replace、search、split

// match —— 提取匹配
"abc123def456".match(/\d+/g); // ["123", "456"]

// matchAll —— 迭代所有匹配(ES2020)
let str = "2024-01-15 and 2024-12-25";
for (let m of str.matchAll(/(\d{4})-(\d{2})-(\d{2})/g)) {
    console.log(m[1], m[2], m[3]);
}

// replace —— 替换
"hello world".replace(/world/, "JS"); // "hello JS"
// 用 $1 $2 引用分组
"2024-01-15".replace(/(\d{4})-(\d{2})-(\d{2})/, "$2/$3/$1");
// "01/15/2024"

// search —— 返回位置
"hello".search(/l/);  // 2
"hello".search(/z/);  // -1

// split —— 用正则分割
"a,b;c".split(/[,;]/); // ["a", "b", "c"]

Part 3: 实战场景

正则用得最多的场景:验证格式、提取信息、替换文本

语法和方法都学了,现在来干正事。邮箱格式对不对?手机号是不是 11 位?密码够不够强?这些验证用正则写最干净。日志里提取时间戳、批量替换日期格式、千分位格式化数字——也是正则的拿手好戏。最后再学一个"零宽断言"的高级技巧,让你的正则功力再上一层。

格式验证

邮箱、手机号、身份证——项目中的高频需求

// 邮箱验证
const emailReg = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
emailReg.test("user@example.com"); // true

// 手机号(中国大陆)
const phoneReg = /^1[3-9]\d{9}$/;
phoneReg.test("13812345678"); // true

// 身份证号(18位)
const idCardReg = /^\d{17}[\dXx]$/;

// 密码强度(至少8位,含大小写字母和数字)
const strongPwd = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
strongPwd.test("Abc12345"); // true
正则验证邮箱只能做基础过滤。 真正严格的邮箱验证需要发确认邮件。不要试图写一个"完美"的邮箱正则,它比你想象的复杂得多。

提取与替换

从文本中抽信息、批量改格式

// 去除首尾空格
"  hello  ".replace(/^\s+|\s+$/g, ""); // "hello"

// 千分位格式化
function formatNumber(num) {
    return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
formatNumber(1234567); // "1,234,567"

// 提取 HTML 标签内容
let html = '<h1>标题</h1><p>段落</p>';
let tags = html.match(/<(\w+)>.*?<\/\1>/g);

常见问题

正则写错了不会报错,只是匹配不到。 这是正则最难调试的地方。推荐用 regex101.com 在线测试。

问题一:正则匹配了不该匹配的内容

原因:量词默认是"贪婪"的,会尽量多匹配。

修复:在量词后加 ? 变成"非贪婪"模式。如 .*? 只匹配尽可能少的字符。

问题二:忘记加 ^ 和 $ 导致部分匹配

原因:/\d+/ 能匹配 "abc123def" 中的 "123"。

修复:验证时加 ^ 和 $,如 /^\d+$/ 确保整个字符串都是数字。

问题三:特殊字符没转义

原因:.、*、? 等在正则中有特殊含义。

修复:用 \ 转义。如匹配 "C:\Users" 要写成 /C:\\Users/。

零宽断言

只判断"旁边是什么",不消耗字符

写法名称含义
(?=...)正向预查右边必须匹配
(?!...)负向预查右边不能匹配
(?<=...)正向后行断言左边必须匹配(ES2018)
(?<!...)负向后行断言左边不能匹配(ES2018)
// 匹配 HTML 标签内的文字(不含标签本身)
let html = "<h1>标题</h1><p>段落</p>";
let texts = html.match(/(?<=<\w+>)[^<]+/g);
console.log(texts); // ["标题", "段落"]

// 密码强度:至少包含大写、小写、数字
const strongPwd = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
console.log(strongPwd.test("Abc12345"));  // true
console.log(strongPwd.test("abcdefgh")); // false

// 负向后行断言:匹配不在 $ 后面的数字(要同时排除 $ 和前面的数字)
let price = "价格 $100 和 200 元";
console.log(price.match(/(?<![$\d])\d+/g)); // ["200"]
零宽断言不消耗字符。 断言匹配的是一个"位置",匹配后指针不会移动。这意味着你可以连续叠加多个断言来组合条件。

Unicode 正则

用 u 修饰符处理中文、emoji 等多字节字符

// 没有 u 修饰符时,. 无法正确匹配 emoji
let emoji = "你好🌍世界";
console.log(emoji.match(/./g).length);   // 6(emoji 被拆成两个码元)
console.log(emoji.match(/./gu).length);  // 4(正确)

// Unicode 属性转义 —— 匹配中文字符
let str = "Hello 世界 123";
console.log(str.match(/\p{Script=Han}/gu));   // ["世", "界"]
console.log(str.match(/\p{L}/gu));            // 所有字母和汉字
console.log(str.match(/\p{N}/gu));            // ["1", "2", "3"]

// 匹配 emoji
let msg = "开心😊难过😢加油💪";
let emojis = msg.match(/\p{Emoji_Presentation}/gu);
console.log(emojis); // ["😊", "😢", "💪"]
Unicode 属性转义必须配合 u 修饰符。 单独写 \p{L} 不加 u 会报语法错误。Node.js 10+ 和现代浏览器均已支持。

试一试

练习一

任务:写一个正则,验证密码强度——至少 8 位,必须包含字母和数字。

// 提示:用 (?=...) 正向预查
// 你的代码:

参考答案

const pwdReg = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/;
console.log(pwdReg.test("abc12345"));  // true
console.log(pwdReg.test("abcdefgh")); // false(没有数字)
console.log(pwdReg.test("12345678")); // false(没有字母)
console.log(pwdReg.test("ab1"));      // false(太短)

练习二

任务:用正则把字符串中的所有连续空格替换成单个空格。

// 提示:用 \s+ 匹配连续空白
// 你的代码:

参考答案

let text = "hello    world   foo    bar";
let result = text.replace(/\s+/g, " ");
console.log(result); // "hello world foo bar"

练习三

任务:写一个正则函数 parseTemplate(tpl, data),把模板字符串中的 {{key}} 替换为 data 对象中对应的值。支持嵌套属性 {{user.name}},如果属性不存在则替换为空字符串。

// 提示:用 /\{\{([^}]+)\}\}/g 匹配占位符
// 然后用 replace 的回调函数替换
// 你的代码:

参考答案

function parseTemplate(tpl, data) {
    return tpl.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
        let value = key.trim().split(".").reduce(
            (obj, k) => (obj != null ? obj[k] : undefined), data
        );
        return value != null ? String(value) : "";
    });
}

let tpl = "你好,{{user.name}}!你有{{count}}条新消息。";
let data = { user: { name: "小明" }, count: 3 };
console.log(parseTemplate(tpl, data));
// "你好,小明!你有3条新消息。"

console.log(parseTemplate("{{missing}}", {}));
// ""(属性不存在,替换为空字符串)

📝 要点总结

本章核心知识点

创建方式

字面量 /pattern/flags 用于固定模式,new RegExp() 用于动态拼接

字符类与量词

\d \w \s 匹配字符类别,* + ? {n,m} 控制重复次数

锚点与分组

^ $ \b 定位边界,() 分组捕获,(?:) 非捕获分组

常用方法

test() 验证、match() 提取、replace() 替换、split() 分割

高级特性

贪婪/懒惰匹配、正向预查 (?=)、命名捕获组、修饰符 g i m u s

下一章

正则基础已经就绪。下一章学习工具与代码质量——用 ESLint、Prettier、测试框架让代码更专业。

1 / X