JavaScript 正则表达式完整教程:从入门到实战案例

runyu
runyu 正式会员正式会员认证极客认证极客
发布于 2026-10-21 00:25 ·3 浏览 ·2 回复
内容摘要

该教程系统讲解 JavaScript 正则的两种创建方式、常用 flag、元字符与量词、分组捕获和命名分组、边界与断言,并给出手机号校验、千分位格式化、驼峰转下划线、URL 参数解析、密码强度等实战写法,最后提醒带 g 的正则存在 lastIndex 状态,连续调用 test 或使用 match 时需注意重置或新建正则。

学完这篇,你能自己写出常用的校验、提取、替换正则,并看懂别人写的复杂表达式。

第一步:先搞清两种创建方式

const re1 = /abc/g;            // 字面量,编译期解析,性能好,推荐
const re2 = new RegExp("abc", "g"); // 动态拼接时必须用这个

四个常用 flag:

  • g 全局匹配(不加只找第一个)
  • i 忽略大小写
  • m 多行模式,让 ^ $ 匹配每行首尾
  • s dotAll,让 . 能匹配换行符

注意:字面量里的内容不要再套引号,/"abc"/ 匹配的是带双引号的字符串。

第二步:记住这几个高频元字符

写法含义
\d / \D数字 / 非数字
\w / \W字母数字下划线 / 其反
\s / \S空白 / 非空白
.任意字符(默认不含换行)
[abc]其中任一字符
[^abc]除这些之外

量词:{n} 恰好 n 次、{n,m} 范围、+ 一次以上、* 零次以上、? 零或一次。

第三步:分组与捕获

圆括号既分组又捕获:

const m = "2024-06-01".match(/(\d{4})-(\d{2})-(\d{2})/);
// m[1]='2024'  m[2]='06'  m[3]='01'

分组多了容易数位置,用命名分组更清楚:

const m = "2024-06-01".match(/(?<y>\d{4})-(?<mo>\d{2})-(?<d>\d{2})/);
console.log(m.groups.y); // 2024

不想捕获就写 (?:...),能少占一个编号。

第四步:边界和断言

  • ^ 开头、$ 结尾、\b 单词边界
  • (?=...) 先行断言:后面必须是它,但不消耗字符
  • (?<=...) 后行断言:前面必须是它
"100px 200em".match(/\d+(?=px)/); // ['100']
"¥88 $99".match(/(?<=¥)\d+/);    // ['88']

注意:^...$ 忘加 m 时,整段有多行文字就永远匹配不上;而校验整串输入时,不加 ^ $ 会导致「部分匹配也算通过」,比如 /\d+/ 校验手机号,abc123 也会返回 true。

第五步:贪婪和懒惰

量词默认贪婪,尽可能多吃。加 ? 变懒惰:

"<p>a</p><p>b</p>".match(/<p>.*<\/p>/)[0];  // 吃到最后一个 </p>
"<p>a</p><p>b</p>".match(/<p>.*?<\/p>/)[0]; // 只吃第一个

抓 HTML 片段、日志切分时,忘记加 ? 是最常见的 bug 来源。

第六步:五个实战案例

1. 手机号校验(中国大陆)

const PHONE = /^1[3-9]\d{9}$/;

2. 千分位分隔

const format = n => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
format(1234567); // '1,234,567'

3. 驼峰转下划线

"userName".replace(/([A-Z])/g, "_$1").toLowerCase(); // user_name

4. 解析 URL 参数

const parseQuery = url => {
  const q = url.split("?")[1] || "";
  return Object.fromEntries(
    [...q.matchAll(/([^&=]+)=([^&]*)/g)].map(([, k, v]) =>
      [k, decodeURIComponent(v)]
    )
  );
};

matchAll 要求正则带 g,否则直接抛错。

5. 密码强度:至少 8 位,含字母和数字

const PWD = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d!@#$%^&*]{8,}$/;

两个先行断言分别检查「有没有字母」「有没有数字」,最后才消费字符。

第七步:三个必踩的坑

注意:带 g 的正则对象有 lastIndex 状态,test() 连续调用会在 true/false 之间跳。要么每次新建正则,要么手动 re.lastIndex = 0。String.prototype.match 配 g 时只返回整体匹配数组,拿不到分组,要分组用 matchAll 或去掉 g 用 exec。

注意:用户输入的字符串要拼进 new RegExp() 前先转义:str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),否则 ( 之类字符会直接让构造抛错。

注意:正则不是万能的。解析嵌套 HTML、复杂 JSON 请用 DOM / JSON.parse,正则只在结构简单稳定时可靠。

小结

  • 静态规则用字面量,动态拼接用 new RegExp 并先转义
  • 校验整串一定要 ^...$,多行文本记得 m
  • 要取分组就用命名分组 (?<name>),只分组不取值用 (?:)
  • 匹配「最短一段」时别忘了懒惰量词 *?
  • g 正则有状态,注意 lastIndex 和 match / matchAll 的差别
  • 建议先在控制台用 "测试字符串".match(/正则/) 试,再写进代码
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-916.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 2

一只冷漠的狐狸
一只冷漠的狐狸 正式会员正式会员认证极客认证极客 钻石卡会员钻石卡会员 1楼 2026-10-21 00:29

这篇梳理得很清楚,只是第六步第二个案例正好断在关键处——千分位那句 replace 没写完,我先补全,再提两个教程里通常不写、但实际中经常遇到的坑。

千分位最稳的写法是用「位置断言」而不是匹配数字,代码如下:

const format = n => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
format(1234567); // '1,234,567'

这里的 \B 保证不会在字符串最前面插入逗号;(?=(\d{3})+(?!\d)) 的意思是:当前位置后面正好剩 3 的倍数个数字,并且已经到结尾。整条正则只插入逗号,不消耗任何数字,所以能一次处理完所有位置。如果还要处理小数,可以在前面加 (\.\d+)? 来截断。

第一个坑是:带 g 标志的正则对象有状态。test 和 exec 每次都会推进 lastIndex,复用一个对象就会出现「第一次 true、第二次 false」这种奇怪现象:

const re = /\d+/g;
re.test('a1'); // true, lastIndex 变成 2
re.test('a1'); // false

要么每次新建正则,要么在 test 前手动把 re.lastIndex 归零。封装成常量复用时,特别容易踩到这个问题。

第二个坑是:replace 的第二个参数用字符串时,$&、$1、$<name> 都有特殊含义。如果替换内容是用户输入或变量拼出来的,就得传函数 replace(re, () => str),否则 $& 会被当成「整个匹配」而吞掉。

延伸一句:抓取 HTML 片段时正则够用,但遇到嵌套标签就翻车了,建议直接上 DOMParser。另外,\d 只认 ASCII 数字,如果涉及全角或阿拉伯数字,就要换成 Unicode 属性转义 \p{Nd} 并加上 u 标志。

jsntccc
jsntccc 正式会员正式会员认证极客认证极客 #1088 2楼 2026-10-21 00:33
一只冷漠的狐狸:这篇梳理得很清楚,只是第六步第二个案例正好断在关键处——千分位那句 `replace` 没写完,我先补全,再提两个教程里通常不写、但实际中经常遇到的坑。 千分…

补得很到位,千分位那条我一般也是用位置断言写的——它只插逗号、不吃字符,所以一次 replace 就能把所有位置处理完,这点是关键。

关于小数那句我跟你有个小分歧:原式其实已经天然避开了小数点。. 不是 word 字符,小数点两侧都是 \b,\B 自然不成立,所以 1234567.89 直接输出 1,234,567.89,不用额外截断。要更保险(科学计数法、负数、超长尾数)不如直接 split('.') 拆开,只格式化整数位再拼回去,比在正则里加分支好维护。

lastIndex 那个坑再收窄一下:真正会咬人的只有 test / exec。String.prototype.match 和 replace 带 g 时都会先把 lastIndex 归零,所以拿同一个常量正则去 str.replace(...) 是安全的,需要小心的是复用同一个带 g 的对象做循环校验、或者在 while (re.exec(s)) 里中途中断。