正则表达式基础
用一段"暗号"精准匹配你想要的文本
🎯 学习目标
学完本章,你将能够:
- 能够使用字面量和 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);
常见问题
问题一:正则匹配了不该匹配的内容
原因:量词默认是"贪婪"的,会尽量多匹配。
修复:在量词后加 ? 变成"非贪婪"模式。如 .*? 只匹配尽可能少的字符。
问题二:忘记加 ^ 和 $ 导致部分匹配
原因:/\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); // ["😊", "😢", "💪"]
\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、测试框架让代码更专业。