导读:本文系统讲解JavaScript正则表达式的完整知识体系,涵盖正则基础语法、JS中正则的应用、表单验证实战,以及高频事件处理中的防抖与节流技术。每个知识点均配完整可运行示例,适合已掌握JavaScript基础、希望深入理解字符串处理与性能优化的开发者。
目录
- 零、导读与学习价值
- [0.1 配套示例覆盖清单](#0.1 配套示例覆盖清单)
- [0.2 核心名词速查](#0.2 核心名词速查)
- [0.3 与前后续章节衔接](#0.3 与前后续章节衔接)
- [0.4 建议练习路线](#0.4 建议练习路线)
- [0.5 模块增量与验收](#0.5 模块增量与验收)
- [0.6 常用正则速查](#0.6 常用正则速查)
- 一、正则表达式基础语法
- 名词解释
- 概念与底层原理
- [深化:NFA 回溯引擎与灾难性回溯](#深化:NFA 回溯引擎与灾难性回溯)
- [深化:ES2018 新增正则特性](#深化:ES2018 新增正则特性)
- 入门示例:原子与字符类
- 实战示例:数量修饰与贪婪匹配
- 实战示例:位置修饰与模式单元
- [可运行示例(补充):修饰符 i / g / m 对照](#可运行示例(补充):修饰符 i / g / m 对照)
- 二、JavaScript中的正则应用
- 名词解释
- 概念与底层原理
- [深化:
matchAll()替代while exec循环](#深化:matchAll() 替代 while exec 循环) - 入门示例:RegExp对象方法
- 实战示例:String对象正则方法
- 三、表单验证实战
- 四、性能优化:防抖与节流
- 名词解释
- 概念与底层原理
- 深化:事件循环与防抖的关系
- [深化:
requestAnimationFrame节流(动画场景专用)](#深化:requestAnimationFrame 节流(动画场景专用)) - 入门示例:防抖与节流对比
- 实战示例:完整防抖节流实现
- 可运行示例(补充):定时器版节流
- 总结
零、导读与学习价值
正则表达式是处理字符串的强大工具,广泛应用于表单验证、数据爬取、文本替换等场景。掌握正则表达式能极大提升字符串处理效率,配合防抖节流技术,能构建高性能的用户交互体验。
0.1 配套示例覆盖清单
| 子目录序号 | 知识点 | 博客对应章节 |
|---|---|---|
| 01-正则表达式/01-原子 | 原子、字符类、转义 | 一、正则表达式基础语法 |
| 01-正则表达式/02-数量修饰 | 数量修饰符、贪婪匹配 | 一、正则表达式基础语法 |
| 01-正则表达式/03-位置修饰 | 锚点、边界匹配 | 一、正则表达式基础语法 |
| 01-正则表达式/04-模式单元 | 分组、捕获 | 一、正则表达式基础语法 |
| 01-正则表达式/05-模式修饰符 | i/g/m修饰符 | 一、正则表达式基础语法 |
| 02-JS中使用正则/01-正则对象的方法 | RegExp对象、test/exec | 二、JavaScript中的正则应用 |
| 02-JS中使用正则/02-字符串对象的方法 | search/match/replace/split | 二、JavaScript中的正则应用 |
| 02-JS中使用正则/03-模式单元 | 分组捕获与替换 | 二、JavaScript中的正则应用 |
| 02-JS中使用正则/04-表单验证 | 综合验证案例 | 三、表单验证实战 |
| 03-节流和防抖/01-对比演示 | 防抖节流对比 | 四、性能优化:防抖与节流 |
| 03-节流和防抖/02-防抖 | 防抖实现原理 | 四、性能优化:防抖与节流 |
| 03-节流和防抖/03-节流 | 节流实现原理 | 四、性能优化:防抖与节流 |
| 03-节流和防抖/04-防抖函数 | 防抖函数封装 | 四、性能优化:防抖与节流 |
| 03-节流和防抖/05-节流函数 | 节流函数封装 | 四、性能优化:防抖与节流 |
0.2 核心名词速查
| 术语 | 一句话解释 |
|---|---|
| 正则表达式 | 用于匹配字符串模式的强大工具,由原子、修饰符组成 |
| 原子 | 正则表达式的基本单位,一个原子匹配一个字符 |
| 贪婪匹配 | 正则表达式默认尽可能多地匹配字符 |
| NFA 引擎 | JS 正则引擎,通过回溯尝试所有路径;嵌套量词可触发指数回溯 |
| ReDoS | 正则拒绝服务攻击,构造恶意输入触发回溯爆炸耗尽 CPU |
| 命名捕获组 | (?<name>...) ES2018,结果存入 match.groups.name |
| 零宽断言 | 先行 (?=...)、后行 (?<=...) 只验证位置不消耗字符 |
matchAll() |
ES2020,返回全局匹配迭代器,每项含完整分组信息 |
| 防抖(Debounce) | 事件触发后延迟执行,期间再次触发则重新计时 |
| 节流(Throttle) | 限制函数执行频率,确保固定时间间隔执行一次 |
| rAF 节流 | 用 requestAnimationFrame 与屏幕刷新率同步,动画专用 |
| 宏任务 | setTimeout/事件回调等;执行完一个才取下一个进栈 |
0.3 与前后续章节衔接
前置知识:读者应具备JavaScript基础语法、DOM操作、事件处理基础。
后续延伸:本文为后续学习表单框架验证(如VeeValidate、Yup)、爬虫数据提取、后端路由匹配打下基础。
0.4 建议练习路线
#mermaid-svg-UkyowE14PIiGdNIs{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UkyowE14PIiGdNIs .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UkyowE14PIiGdNIs .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UkyowE14PIiGdNIs .error-icon{fill:#552222;}#mermaid-svg-UkyowE14PIiGdNIs .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UkyowE14PIiGdNIs .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UkyowE14PIiGdNIs .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UkyowE14PIiGdNIs .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UkyowE14PIiGdNIs .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UkyowE14PIiGdNIs .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UkyowE14PIiGdNIs .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UkyowE14PIiGdNIs .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UkyowE14PIiGdNIs .marker.cross{stroke:#333333;}#mermaid-svg-UkyowE14PIiGdNIs svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UkyowE14PIiGdNIs p{margin:0;}#mermaid-svg-UkyowE14PIiGdNIs .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-UkyowE14PIiGdNIs .cluster-label text{fill:#333;}#mermaid-svg-UkyowE14PIiGdNIs .cluster-label span{color:#333;}#mermaid-svg-UkyowE14PIiGdNIs .cluster-label span p{background-color:transparent;}#mermaid-svg-UkyowE14PIiGdNIs .label text,#mermaid-svg-UkyowE14PIiGdNIs span{fill:#333;color:#333;}#mermaid-svg-UkyowE14PIiGdNIs .node rect,#mermaid-svg-UkyowE14PIiGdNIs .node circle,#mermaid-svg-UkyowE14PIiGdNIs .node ellipse,#mermaid-svg-UkyowE14PIiGdNIs .node polygon,#mermaid-svg-UkyowE14PIiGdNIs .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UkyowE14PIiGdNIs .rough-node .label text,#mermaid-svg-UkyowE14PIiGdNIs .node .label text,#mermaid-svg-UkyowE14PIiGdNIs .image-shape .label,#mermaid-svg-UkyowE14PIiGdNIs .icon-shape .label{text-anchor:middle;}#mermaid-svg-UkyowE14PIiGdNIs .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UkyowE14PIiGdNIs .rough-node .label,#mermaid-svg-UkyowE14PIiGdNIs .node .label,#mermaid-svg-UkyowE14PIiGdNIs .image-shape .label,#mermaid-svg-UkyowE14PIiGdNIs .icon-shape .label{text-align:center;}#mermaid-svg-UkyowE14PIiGdNIs .node.clickable{cursor:pointer;}#mermaid-svg-UkyowE14PIiGdNIs .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UkyowE14PIiGdNIs .arrowheadPath{fill:#333333;}#mermaid-svg-UkyowE14PIiGdNIs .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UkyowE14PIiGdNIs .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UkyowE14PIiGdNIs .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UkyowE14PIiGdNIs .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UkyowE14PIiGdNIs .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UkyowE14PIiGdNIs .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UkyowE14PIiGdNIs .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UkyowE14PIiGdNIs .cluster text{fill:#333;}#mermaid-svg-UkyowE14PIiGdNIs .cluster span{color:#333;}#mermaid-svg-UkyowE14PIiGdNIs div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UkyowE14PIiGdNIs .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UkyowE14PIiGdNIs rect.text{fill:none;stroke-width:0;}#mermaid-svg-UkyowE14PIiGdNIs .icon-shape,#mermaid-svg-UkyowE14PIiGdNIs .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UkyowE14PIiGdNIs .icon-shape p,#mermaid-svg-UkyowE14PIiGdNIs .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UkyowE14PIiGdNIs .icon-shape .label rect,#mermaid-svg-UkyowE14PIiGdNIs .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UkyowE14PIiGdNIs .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UkyowE14PIiGdNIs .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UkyowE14PIiGdNIs :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 01 正则语法
02 JS API
04 表单验证
03 防抖节流
【代码注释】先掌握「怎么写模式」,再掌握「JS 怎么调」,最后用防抖优化验证与搜索;防抖与正则无语法依赖,但表单验证章节会同时用到两者。
| 阶段 | 示例主题 | 验收标准 |
|---|---|---|
| 语法 | 原子 → 量词 → 位置 → 分组 → 修饰符 | 能解释 ^ $ () i g m |
| API | test / exec / match / replace |
能格式化手机号、替换日期分隔符 |
| 验证 | 邮箱、手机、用户名 | blur + 防抖实时校验通过 |
| 性能 | 防抖、节流封装 | apply(this, args) 正确,搜索 400ms 防抖 |
按「正则语法」→「JS API」→「防抖节流」顺序练习;表单验证可先做基础验证示例,再回头用防抖包装 input 事件。
0.5 模块增量与验收
| 模块 | 新增能力 | 与上一模块关系 |
|---|---|---|
| 正则基础语法 | 写模式:原子、量词、锚点、分组、修饰符 | 独立基础 |
| JS 正则 API | 调 API:RegExp + String 六方法 |
依赖正则模式 |
| 表单验证 | 业务规则 + DOM 反馈 | 依赖 test() |
| 防抖与节流 | 高阶函数包装回调 | 可套在验证、搜索、scroll 上 |
【代码注释】03 与 01 无直接语法关系,但电商详情页「规格区 timeStamp 节流」与本章「时间戳节流」是同一类思路;搜索联想则必须用防抖。
0.6 常用正则速查
以下摘自配套「常用正则」清单,生产环境建议用成熟库(如 libphonenumber),此处用于理解写法与课堂对齐:
| 场景 | 模式(精简) | 说明 |
|---|---|---|
| 11 位手机 | ^1[3-9]\d{9}$ |
与课堂表单一致,未覆盖所有号段 |
| 邮箱(课堂) | ^[\w-]+@[\w-]+(\.\w+){1,2}$ |
\w 含下划线;国际化邮箱需更复杂规则 |
| 用户名 | ^[a-zA-Z][a-zA-Z0-9_]{4,15}$ |
字母开头 5~16 位 |
| 纯数字 n 位 | ^\d{n}$ |
验证码等 |
| 汉字 | [\u4e00-\u9fa5] |
常用 Unicode 范围 |
| 去首尾空白 | replace(/^\s+/, '') + replace(/\s+$/, '') |
分两步去行首行尾空白 |
| QQ 号 | ^[1-9][0-9]{4,}$ |
5 位及以上 |
| 6 位邮编 | ^[1-9]\d{5}(?!\d)$ |
(?!\d) 否定前瞻,避免 7 位 |
【代码注释】| 在字符类外是「或」,在 [] 内是字面量;写错位置会导致语义完全变化。复杂身份证、URL 等见 MDN 与专用校验库,不要照搬网上过时片段。
一、正则表达式基础语法
名词解释
- 原子:正则表达式的基本组成单位,一个原子匹配字符串中的一个字符
- 字符类 :用方括号
[]定义的字符集合,一个字符类原子匹配范围内的任意一个字符 - 数量修饰符 :控制原子出现次数的符号,如
*、+、?、{n,m} - 位置修饰符 :匹配字符串位置而非字符的符号,如
^、$、\b - 贪婪匹配:默认匹配模式,尽可能多地匹配字符
- NFA 引擎:非确定性有限自动机,JavaScript 正则引擎类型,通过回溯尝试所有匹配路径
- 回溯(Backtracking):NFA 引擎在路径失败时退回决策点尝试其他分支的机制
- 灾难性回溯(ReDoS):嵌套量词导致引擎回溯路径呈指数增长,CPU 耗尽
- 断言(Assertion) :零宽匹配,只验证位置不消耗字符,如先行
(?=...)和后行(?<=...) - 命名捕获组 :
(?<name>...)将分组命名,结果存入match.groups对象(ES2018)
概念与底层原理
正则表达式的核心原理是模式匹配:将正则表达式与目标字符串进行比对,找出符合模式的子串。
#mermaid-svg-4AqdJO21w5dWV5vQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4AqdJO21w5dWV5vQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4AqdJO21w5dWV5vQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4AqdJO21w5dWV5vQ .error-icon{fill:#552222;}#mermaid-svg-4AqdJO21w5dWV5vQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4AqdJO21w5dWV5vQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4AqdJO21w5dWV5vQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4AqdJO21w5dWV5vQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4AqdJO21w5dWV5vQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4AqdJO21w5dWV5vQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4AqdJO21w5dWV5vQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4AqdJO21w5dWV5vQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4AqdJO21w5dWV5vQ .marker.cross{stroke:#333333;}#mermaid-svg-4AqdJO21w5dWV5vQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4AqdJO21w5dWV5vQ p{margin:0;}#mermaid-svg-4AqdJO21w5dWV5vQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4AqdJO21w5dWV5vQ .cluster-label text{fill:#333;}#mermaid-svg-4AqdJO21w5dWV5vQ .cluster-label span{color:#333;}#mermaid-svg-4AqdJO21w5dWV5vQ .cluster-label span p{background-color:transparent;}#mermaid-svg-4AqdJO21w5dWV5vQ .label text,#mermaid-svg-4AqdJO21w5dWV5vQ span{fill:#333;color:#333;}#mermaid-svg-4AqdJO21w5dWV5vQ .node rect,#mermaid-svg-4AqdJO21w5dWV5vQ .node circle,#mermaid-svg-4AqdJO21w5dWV5vQ .node ellipse,#mermaid-svg-4AqdJO21w5dWV5vQ .node polygon,#mermaid-svg-4AqdJO21w5dWV5vQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4AqdJO21w5dWV5vQ .rough-node .label text,#mermaid-svg-4AqdJO21w5dWV5vQ .node .label text,#mermaid-svg-4AqdJO21w5dWV5vQ .image-shape .label,#mermaid-svg-4AqdJO21w5dWV5vQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-4AqdJO21w5dWV5vQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4AqdJO21w5dWV5vQ .rough-node .label,#mermaid-svg-4AqdJO21w5dWV5vQ .node .label,#mermaid-svg-4AqdJO21w5dWV5vQ .image-shape .label,#mermaid-svg-4AqdJO21w5dWV5vQ .icon-shape .label{text-align:center;}#mermaid-svg-4AqdJO21w5dWV5vQ .node.clickable{cursor:pointer;}#mermaid-svg-4AqdJO21w5dWV5vQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4AqdJO21w5dWV5vQ .arrowheadPath{fill:#333333;}#mermaid-svg-4AqdJO21w5dWV5vQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4AqdJO21w5dWV5vQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4AqdJO21w5dWV5vQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4AqdJO21w5dWV5vQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4AqdJO21w5dWV5vQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4AqdJO21w5dWV5vQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4AqdJO21w5dWV5vQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4AqdJO21w5dWV5vQ .cluster text{fill:#333;}#mermaid-svg-4AqdJO21w5dWV5vQ .cluster span{color:#333;}#mermaid-svg-4AqdJO21w5dWV5vQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4AqdJO21w5dWV5vQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4AqdJO21w5dWV5vQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-4AqdJO21w5dWV5vQ .icon-shape,#mermaid-svg-4AqdJO21w5dWV5vQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4AqdJO21w5dWV5vQ .icon-shape p,#mermaid-svg-4AqdJO21w5dWV5vQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4AqdJO21w5dWV5vQ .icon-shape .label rect,#mermaid-svg-4AqdJO21w5dWV5vQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4AqdJO21w5dWV5vQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4AqdJO21w5dWV5vQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4AqdJO21w5dWV5vQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 成功
失败
正则表达式
正则引擎
目标字符串
匹配结果
返回匹配信息
返回null/false
【代码注释】流程图展示正则匹配过程:正则引擎将正则模式与目标字符串比对,成功返回匹配信息(如匹配位置、捕获分组),失败返回null/false。理解此流程有助于掌握正则在JavaScript中的应用逻辑。
深化:NFA 回溯引擎与灾难性回溯
JavaScript 使用 NFA(非确定性有限自动机)引擎:
JavaScript(V8 的 Irregexp 引擎)采用 NFA 回溯(backtracking) 算法匹配正则:当一条路径失败时,引擎"回头"尝试另一种分支。大多数情况下速度很快,但特定模式会触发指数级回溯。
#mermaid-svg-7kJlUa8yW1NEH8he{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7kJlUa8yW1NEH8he .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7kJlUa8yW1NEH8he .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7kJlUa8yW1NEH8he .error-icon{fill:#552222;}#mermaid-svg-7kJlUa8yW1NEH8he .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7kJlUa8yW1NEH8he .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7kJlUa8yW1NEH8he .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7kJlUa8yW1NEH8he .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7kJlUa8yW1NEH8he .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7kJlUa8yW1NEH8he .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7kJlUa8yW1NEH8he .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7kJlUa8yW1NEH8he .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7kJlUa8yW1NEH8he .marker.cross{stroke:#333333;}#mermaid-svg-7kJlUa8yW1NEH8he svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7kJlUa8yW1NEH8he p{margin:0;}#mermaid-svg-7kJlUa8yW1NEH8he .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7kJlUa8yW1NEH8he .cluster-label text{fill:#333;}#mermaid-svg-7kJlUa8yW1NEH8he .cluster-label span{color:#333;}#mermaid-svg-7kJlUa8yW1NEH8he .cluster-label span p{background-color:transparent;}#mermaid-svg-7kJlUa8yW1NEH8he .label text,#mermaid-svg-7kJlUa8yW1NEH8he span{fill:#333;color:#333;}#mermaid-svg-7kJlUa8yW1NEH8he .node rect,#mermaid-svg-7kJlUa8yW1NEH8he .node circle,#mermaid-svg-7kJlUa8yW1NEH8he .node ellipse,#mermaid-svg-7kJlUa8yW1NEH8he .node polygon,#mermaid-svg-7kJlUa8yW1NEH8he .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7kJlUa8yW1NEH8he .rough-node .label text,#mermaid-svg-7kJlUa8yW1NEH8he .node .label text,#mermaid-svg-7kJlUa8yW1NEH8he .image-shape .label,#mermaid-svg-7kJlUa8yW1NEH8he .icon-shape .label{text-anchor:middle;}#mermaid-svg-7kJlUa8yW1NEH8he .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7kJlUa8yW1NEH8he .rough-node .label,#mermaid-svg-7kJlUa8yW1NEH8he .node .label,#mermaid-svg-7kJlUa8yW1NEH8he .image-shape .label,#mermaid-svg-7kJlUa8yW1NEH8he .icon-shape .label{text-align:center;}#mermaid-svg-7kJlUa8yW1NEH8he .node.clickable{cursor:pointer;}#mermaid-svg-7kJlUa8yW1NEH8he .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7kJlUa8yW1NEH8he .arrowheadPath{fill:#333333;}#mermaid-svg-7kJlUa8yW1NEH8he .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7kJlUa8yW1NEH8he .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7kJlUa8yW1NEH8he .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7kJlUa8yW1NEH8he .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7kJlUa8yW1NEH8he .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7kJlUa8yW1NEH8he .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7kJlUa8yW1NEH8he .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7kJlUa8yW1NEH8he .cluster text{fill:#333;}#mermaid-svg-7kJlUa8yW1NEH8he .cluster span{color:#333;}#mermaid-svg-7kJlUa8yW1NEH8he div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7kJlUa8yW1NEH8he .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7kJlUa8yW1NEH8he rect.text{fill:none;stroke-width:0;}#mermaid-svg-7kJlUa8yW1NEH8he .icon-shape,#mermaid-svg-7kJlUa8yW1NEH8he .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7kJlUa8yW1NEH8he .icon-shape p,#mermaid-svg-7kJlUa8yW1NEH8he .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7kJlUa8yW1NEH8he .icon-shape .label rect,#mermaid-svg-7kJlUa8yW1NEH8he .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7kJlUa8yW1NEH8he .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7kJlUa8yW1NEH8he .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7kJlUa8yW1NEH8he :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
开始匹配
尝试第一条路径
成功?
返回结果
回溯到上一个决策点
还有备用路径?
尝试下一条路径
整体匹配失败
【代码注释】NFA 引擎的「回溯」机制:每遇到量词(*、+、?)或分支(|)都产生决策点。失败时原路退回,尝试不同数量或不同分支。多数正则只有少量决策点,性能极好;但嵌套量词可使决策点数量呈指数增长。
灾难性回溯(Catastrophic Backtracking / ReDoS):
javascript
// 危险模式:嵌套量词 + 末尾强制失败
var dangerous = /^(a+)+$/;
// 匹配 "aaab" 时:
// 引擎需要尝试 (a+) 分组的 2^n 种分配方式,全部以 b 失败告终
console.log(dangerous.test('aaaaaaaaaaaaaab'));
// 会造成 CPU 100%,甚至挂起浏览器/Node.js 进程
【代码注释】(a+)+ 中外层 + 每次迭代,内层 a+ 都可以分配不同数量,形成指数级路径。最后的 b 确保所有路径都失败,引擎穷举完所有可能才返回 false。这是 ReDoS(正则拒绝服务攻击) 的原理------攻击者构造恶意输入,使服务器正则耗尽 CPU。
ReDoS 预防原则:
| 危险写法 | 原因 | 安全替代 |
|---|---|---|
(a+)+ |
嵌套量词 | a+(合并层级) |
(a*)* |
嵌套量词 | a* |
| `(a | a)+` | 重叠交替 |
.*.*end |
双 .* 分配歧义 |
.*end(一个) |
\w+\s*=\s*\w+ |
可接受,量词无嵌套 | --- |
javascript
// 安全的手机号验证(锚点 + 具体字符类,无嵌套量词)
var safePhone = /^1[3-9]\d{9}$/;
// 安全的邮箱验证(分段明确,无模糊路径)
var safeEmail = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
【代码注释】表单验证正则的安全要素:① 加锚点 ^ $ 避免部分匹配;② 用具体字符类([3-9]、\d)而非模糊 .;③ 量词不嵌套({9} 比 + 在固定长度场景更安全)。V8 8.8+ 引入了线性非回溯引擎,但目前仅对不含 backreference / lookaround 的正则生效。
深化:ES2018 新增正则特性
1. 命名捕获组 (?<name>...)(ES2018)
javascript
// 传统:按索引取分组,含义不明
var dateReg = /(\d{4})-(\d{2})-(\d{2})/;
var m = '2025-01-15'.match(dateReg);
console.log(m[1], m[2], m[3]); // '2025' '01' '15'
// 命名捕获组:按名字取,语义清晰
var namedReg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
var nm = '2025-01-15'.match(namedReg);
console.log(nm.groups.year); // '2025'
console.log(nm.groups.month); // '01'
// 在 replace 中用 $<name> 引用命名分组
var result = '2025-01-15'.replace(namedReg, '$<year>/$<month>/$<day>');
console.log(result); // '2025/01/15'
【代码注释】(?<year>...) 将分组命名为 year,结果存入 match.groups 对象。命名分组让正则的意图一目了然,且重构时修改分组顺序不会破坏引用代码。replace 中用 $<name> 而非 $1 引用,可读性更高。市面应用:日期格式化、URL 解析、数据提取管道。
2. 后行断言(Lookbehind Assertions,ES2018)
javascript
// 正向先行断言 (?=...) --- ES3 已有,"后面是什么"
var priceReg = /\d+(?=元)/;
console.log('100元商品'.match(priceReg)[0]); // '100'(不含"元")
// 负向先行断言 (?!...) --- ES3 已有,"后面不是什么"
var notDotReg = /\d+(?!\.)/g;
console.log('3.14和42'.match(notDotReg)); // ['14', '42'](前面没跟在 . 后的数字)
// 正向后行断言 (?<=...) --- ES2018,"前面是什么"
var afterSymbol = /(?<=¥)\d+/;
console.log('¥100'.match(afterSymbol)[0]); // '100'(只取货币符后的数字)
// 负向后行断言 (?<!...) --- ES2018,"前面不是什么"
var notAfterHash = /(?<!#)\d+/g;
console.log('#100 200'.match(notAfterHash)); // ['200'](排除 # 后的数字)
【代码注释】断言(Assertions)是零宽匹配 ,只验证位置、不消耗字符。先行断言向右看((?=...) 正向、(?!...) 负向);后行断言向左看((?<=...) 正向、(?<!...) 负向)。ES2018 之前 JS 缺少后行断言,提取"货币符号后的数字"只能用分组后再取子串,现在直接用 (?<=¥) 更简洁。市面应用:价格提取、日志字段解析、避免误匹配注释中的数字。
3. s 修饰符(dotAll,ES2018)
javascript
// 默认:. 不匹配换行符
var html = '<div>\n内容\n</div>';
console.log(/<div>.*<\/div>/.test(html)); // false(跨行失败)
// 加 s 修饰符:. 匹配所有字符包括换行
console.log(/<div>.*<\/div>/s.test(html)); // true
【代码注释】s(dotAll)让 . 也匹配 \n、\r,解决多行 HTML 内容匹配的痛点。之前的替代写法是 [\s\S]*(字符类合并空白与非空白覆盖所有字符),/s 更语义化。与 m 修饰符不冲突,可组合为 /sm。
正则表达式由以下部分组成:
- 原子:基本匹配单位
- 数量修饰符:控制原子出现次数
- 位置修饰符:匹配字符串位置
- 模式单元:分组与捕获
- 模式修饰符:全局匹配模式
【实战要点】
- 经典应用场景:表单验证(邮箱、手机号、身份证)、关键词高亮、数据清洗、日志分析
- 常见坑 :忘记转义特殊字符
.、*、+等;贪婪匹配导致过度匹配;未使用锚点导致部分匹配 - 性能与最佳实践 :使用锚点
^、$快速失败;具体化字符类避免.;使用字面量而非RegExp构造函数;避免嵌套量词导致回溯爆炸
入门示例:原子与字符类
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>原子与字符类</title>
</head>
<body>
<script>
// 1. 普通原子:一个字母、数字、汉字都是一个原子
console.log(/h/.test('h')); // true
console.log(/h/.test('hello')); // true
console.log(/乐/.test('快乐')); // true
// 2. 特殊符号需要转义
console.log(/\//.test('plat/learner')); // true
console.log(/\./.test('hello.world')); // true
// 3. 字符类:匹配范围内任意一个字符
console.log(/[abc]/.test('apple')); // true,匹配a
console.log(/[0-9]/.test('2025')); // true,匹配数字
console.log(/[a-z]/.test('Hello')); // true,匹配小写字母
// 4. 反向字符类:匹配范围外的字符
console.log(/[^0-9]/.test('a1b2')); // true,匹配字母
console.log(/[^abc]/.test('def')); // true
// 5. 预定义字符类
console.log(/\d/.test('123')); // true,相当于[0-9]
console.log(/\D/.test('abc')); // true,相当于[^0-9]
console.log(/\w/.test('_user')); // true,相当于[a-zA-Z0-9_]
console.log(/\W/.test('@symbol')); // true,相当于[^a-zA-Z0-9_]
// 6. 点号匹配任意字符(除换行符)
console.log(/./.test('你')); // true
console.log(/./.test('\n')); // false,点号不匹配换行
</script>
</body>
</html>
【代码注释】演示原子基本用法:普通字符直接匹配;特殊字符需用\转义;字符类[]匹配范围内任意字符;预定义字符类\d、\w是常用快捷方式。理解原子是掌握正则的第一步,所有复杂模式都由原子组合而成。
实战示例:数量修饰与贪婪匹配
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>数量修饰符</title>
</head>
<body>
<script>
// 1. 精确匹配次数 {n}
console.log(/\d{4}/.test('2025')); // true,4个数字
console.log(/\d{4}/.test('25')); // false
// 2. 范围匹配 {n,m}
console.log(/\d{3,5}/.test('123')); // true,3-5个数字
console.log(/\d{3,5}/.test('123456')); // true,匹配前5个
console.log(/\d{3,5}/.test('12')); // false
// 3. 最少匹配 {n,}
console.log(/\d{2,}/.test('12')); // true,至少2个数字
console.log(/\d{2,}/.test('123456')); // true
// 4. 常用简写
console.log(/colou?r/.test('color')); // true,?表示0或1次
console.log(/colou?r/.test('colour')); // true
console.log(/\d+/.test('123')); // true,+表示1次或多次
console.log(/\d*/.test('abc')); // true,*表示0次或多次
// 5. 贪婪匹配示例
var str = '<div>内容1</div><div>内容2</div>';
console.log(/<div>.*<\/div>/.exec(str)[0]);
// 贪婪匹配整个字符串:'<div>内容1</div><div>内容2</div>'
console.log(/<div>.*?<\/div>/.exec(str)[0]);
// 懒惰匹配:'<div>内容1</div>'(使用?取消贪婪)
</script>
</body>
</html>
【代码注释】数量修饰符控制原子重复次数:{n}精确次数、{n,m}范围、?、+、*是常用简写。正则默认贪婪匹配(尽可能多),在量词后加?转为懒惰匹配。实际开发中,提取HTML内容常用.*?避免过度匹配。市面应用:爬虫提取数据、日志分析中的时间戳匹配。
实战示例:位置修饰与模式单元
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>位置修饰与模式单元</title>
</head>
<body>
<script>
// 1. 位置修饰符 ^ 和 $
console.log(/^\d+$/.test('123')); // true,整个字符串都是数字
console.log(/^\d+$/.test('123abc')); // false
console.log(/^hello/.test('hello world')); // true,以hello开头
console.log(/world$/.test('hello world')); // true,以world结尾
// 2. 单词边界 \b
console.log(/\bcat\b/.test('concatenate')); // false,cat不是独立单词
console.log(/\bcat\b/.test('the cat is')); // true,cat是独立单词
// 3. 模式单元:分组与捕获
var dateStr = '2025-01-15';
var dateReg = /(\d{4})-(\d{2})-(\d{2})/;
var match = dateReg.exec(dateStr);
console.log(match[0]); // '2025-01-15' 完整匹配
console.log(match[1]); // '2025' 第一个分组
console.log(match[2]); // '01' 第二个分组
console.log(match[3]); // '15' 第三个分组
// 4. 非捕获分组 (?:
var reg = /(apple)+\s?(?:orange)+/;
console.log(reg.exec('apple apple orange orange'));
// 非捕获分组不生成单独的捕获项
// 5. 模式修饰符
var str = 'Hello World';
console.log(/hello/.test(str)); // false
console.log(/hello/i.test(str)); // true,i忽略大小写
var str2 = 'apple orange apple';
console.log(str2.match(/apple/)); // 只匹配第一个
console.log(str2.match(/apple/g)); // 匹配所有(全局)
// 6. 多行修饰符 m:^ $ 按行匹配
var msg = 'school\nhello\n52323';
console.log(/^hello$/.test(msg)); // false,整串不是单独 hello
console.log(/^hello$/m.test(msg)); // true,第二行是 hello
</script>
</body>
</html>
【代码注释】位置修饰符^、$匹配字符串起止位置,\b匹配单词边界;模式单元()实现分组与捕获,可用$1、$2在replace中引用;非捕获分组(?:)不生成捕获项;修饰符i忽略大小写、g全局匹配、m多行模式(每一行单独应用 ^ $)。理解边界匹配是高效验证的关键。市面应用 :表单验证/^1[3-9]\d{9}$/匹配手机号。
m 修饰符与多行文本(与配套示例一致):
javascript
var msg = 'school\nhello\n52323';
/^hello$/.test(msg); // false:默认 ^ $ 针对整个字符串
/^hello$/m.test(msg); // true:m 模式下第二行等于 hello
【代码注释】处理 textarea、日志按行校验时常加 m;与 g 可组合为 /^\\d+$/gm。未加 m 时行首 ^ 只认整个字符串开头。
可运行示例(补充):修饰符 i / g / m 对照
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>修饰符对照</title>
</head>
<body>
<script>
console.log(/Hello/.test('hello')); // false
console.log(/Hello/i.test('hello')); // true
var s = 'a\nb\nc';
console.log(/^b$/m.test(s)); // true
console.log('match count:', 'x y x'.match(/x/g).length); // 2
</script>
</body>
</html>
【代码注释】三个示例分别对应忽略大小写、按行锚点、全局收集;写模式时把修饰符写在第二个 / 之后,如 /pattern/gim。
【本章小结】
| 语法 | 功能 | 示例 |
|---|---|---|
| 原子 | 匹配单个字符 | /a/匹配'a' |
| 字符类 | 匹配范围内字符 | /[a-z]/匹配小写字母 |
| 数量修饰符 | 控制原子次数 | /a{2,4}/匹配a出现2-4次 |
| 位置修饰符 | 匹配字符串位置 | /^abc$/精确匹配abc |
| 模式单元 | 分组与捕获 | /(\d{4})/捕获4位数字 |
记忆口诀:"原子是基础,字符类是范围,量词定次数,锚点定位置,分组来实现,修饰来辅助"
【面试考点】
Q1:贪婪匹配与懒惰匹配的区别?
A:贪婪匹配(默认)尽可能多地匹配字符,如a.*b匹配aabab时匹配整个字符串;懒惰匹配(量词后加?)尽可能少地匹配,如a.*?b只匹配aab。实际应用中,提取HTML标签内容常用<div>.*?</div>避免匹配多个div。
Q2:捕获分组与非捕获分组的区别?
A:捕获分组()将匹配结果保存,可用$1、$2在replace中引用,或用match[1]获取;非捕获分组(?:)仅用于分组,不保存结果,性能更优。当不需要引用分组内容时,优先使用非捕获分组。
Q3:m 与不加 m 时 ^hello$ 有何不同?
A:不加 m 时 ^ $ 锚定整个字符串 的首尾,含换行符的字符串很难整行等于 hello;加 m 后每一行视为独立字符串,^hello$ 可匹配某一整行。多行日志、按行校验 ID 时常用 /^pattern$/m。
Q4:什么是 ReDoS?如何预防?
A:ReDoS(正则拒绝服务)是指攻击者构造恶意输入,触发正则 NFA 引擎的指数级回溯,导致 CPU 100%(如 /(a+)+$/ 匹配长 aaa...b)。预防措施:① 避免嵌套量词((a+)+ 改 a+);② 避免重叠交替((a|a)+);③ 用具体字符类代替 .;④ 加锚点 ^ $ 快速失败;⑤ 用 safe-regex 或 vuln-regex-detector 等工具静态检测。服务端正则尤其重要,一个恶意请求可阻塞整个 Node.js 事件循环。
Q5:先行断言与后行断言的区别?命名捕获组的优势?
A:先行断言 (?=...)/(?!...) 向右看,ES3 起支持;后行断言 (?<=...)/(?<!...) 向左看,ES2018 才引入。两者都是零宽断言,不消耗字符。命名捕获组 (?<name>...) 的优势:① match.groups.name 比 match[1] 语义清晰;② replace 中用 $<name> 不受分组顺序影响;③ 正则重构时修改组位置不破坏下游代码。
二、JavaScript中的正则应用
名词解释
- RegExp对象:JavaScript中处理正则表达式的内置对象
- 字面量方式 :使用
/pattern/直接创建正则表达式 - 构造函数方式 :使用
new RegExp('pattern')动态创建正则 - 全局匹配 :使用
g修饰符匹配所有符合模式的内容 matchAll():ES2020 方法,返回全局匹配的迭代器,每项含捕获分组(比while exec更简洁)replaceAll():ES2021 方法,替换所有匹配项(传正则时等价replace(/g/),但更语义化)lastIndex:RegExp 实例属性,记录全局匹配的下次起始位置,影响exec/test结果
概念与底层原理
JavaScript中正则表达式有两种创建方式:
#mermaid-svg-U0aAiZnQZHXeQFmj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-U0aAiZnQZHXeQFmj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-U0aAiZnQZHXeQFmj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-U0aAiZnQZHXeQFmj .error-icon{fill:#552222;}#mermaid-svg-U0aAiZnQZHXeQFmj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-U0aAiZnQZHXeQFmj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-U0aAiZnQZHXeQFmj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-U0aAiZnQZHXeQFmj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-U0aAiZnQZHXeQFmj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-U0aAiZnQZHXeQFmj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-U0aAiZnQZHXeQFmj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-U0aAiZnQZHXeQFmj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-U0aAiZnQZHXeQFmj .marker.cross{stroke:#333333;}#mermaid-svg-U0aAiZnQZHXeQFmj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-U0aAiZnQZHXeQFmj p{margin:0;}#mermaid-svg-U0aAiZnQZHXeQFmj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-U0aAiZnQZHXeQFmj .cluster-label text{fill:#333;}#mermaid-svg-U0aAiZnQZHXeQFmj .cluster-label span{color:#333;}#mermaid-svg-U0aAiZnQZHXeQFmj .cluster-label span p{background-color:transparent;}#mermaid-svg-U0aAiZnQZHXeQFmj .label text,#mermaid-svg-U0aAiZnQZHXeQFmj span{fill:#333;color:#333;}#mermaid-svg-U0aAiZnQZHXeQFmj .node rect,#mermaid-svg-U0aAiZnQZHXeQFmj .node circle,#mermaid-svg-U0aAiZnQZHXeQFmj .node ellipse,#mermaid-svg-U0aAiZnQZHXeQFmj .node polygon,#mermaid-svg-U0aAiZnQZHXeQFmj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-U0aAiZnQZHXeQFmj .rough-node .label text,#mermaid-svg-U0aAiZnQZHXeQFmj .node .label text,#mermaid-svg-U0aAiZnQZHXeQFmj .image-shape .label,#mermaid-svg-U0aAiZnQZHXeQFmj .icon-shape .label{text-anchor:middle;}#mermaid-svg-U0aAiZnQZHXeQFmj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-U0aAiZnQZHXeQFmj .rough-node .label,#mermaid-svg-U0aAiZnQZHXeQFmj .node .label,#mermaid-svg-U0aAiZnQZHXeQFmj .image-shape .label,#mermaid-svg-U0aAiZnQZHXeQFmj .icon-shape .label{text-align:center;}#mermaid-svg-U0aAiZnQZHXeQFmj .node.clickable{cursor:pointer;}#mermaid-svg-U0aAiZnQZHXeQFmj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-U0aAiZnQZHXeQFmj .arrowheadPath{fill:#333333;}#mermaid-svg-U0aAiZnQZHXeQFmj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-U0aAiZnQZHXeQFmj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-U0aAiZnQZHXeQFmj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-U0aAiZnQZHXeQFmj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-U0aAiZnQZHXeQFmj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-U0aAiZnQZHXeQFmj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-U0aAiZnQZHXeQFmj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-U0aAiZnQZHXeQFmj .cluster text{fill:#333;}#mermaid-svg-U0aAiZnQZHXeQFmj .cluster span{color:#333;}#mermaid-svg-U0aAiZnQZHXeQFmj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-U0aAiZnQZHXeQFmj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-U0aAiZnQZHXeQFmj rect.text{fill:none;stroke-width:0;}#mermaid-svg-U0aAiZnQZHXeQFmj .icon-shape,#mermaid-svg-U0aAiZnQZHXeQFmj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-U0aAiZnQZHXeQFmj .icon-shape p,#mermaid-svg-U0aAiZnQZHXeQFmj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-U0aAiZnQZHXeQFmj .icon-shape .label rect,#mermaid-svg-U0aAiZnQZHXeQFmj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-U0aAiZnQZHXeQFmj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-U0aAiZnQZHXeQFmj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-U0aAiZnQZHXeQFmj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 创建正则表达式
字面量方式 /pattern/
构造函数方式 RegExp/pattern/
性能好,适用于固定模式
灵活,适用于动态模式
RegExp对象方法
test方法
exec方法
String对象正则方法
【代码注释】流程图展示JS正则创建方式:字面量/pattern/在脚本加载时编译,性能最佳;RegExp构造函数可动态创建模式,灵活但性能略低。两种方式最终都调用RegExp对象方法(test/exec)或String对象方法(search/match/replace/split)。
RegExp对象与String对象都提供了正则相关方法:
RegExp对象方法:
test(str):返回布尔值,判断是否匹配exec(str):返回匹配结果数组或null
String对象方法:
search(regexp):返回首个匹配的索引,找不到返回-1match(regexp):返回匹配结果数组或nullreplace(regexp, replacement):替换匹配内容replaceAll(string, replacement):替换所有匹配(ES2021,传正则时必须带g)split(regexp):按正则分割字符串为数组matchAll(regexp):返回所有匹配结果的迭代器,含捕获分组(ES2020,必须带g)
深化:matchAll() 替代 while exec 循环
String.prototype.matchAll() 是 ES2020 引入的新方法,解决了 match() 全局模式下丢失捕获分组的痛点:
javascript
var str = '2025-01-15 and 2024-12-31';
var reg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/g;
// 旧写法:while + exec 循环(繁琐)
var m;
while ((m = reg.exec(str)) !== null) {
console.log(m.groups.year, m.groups.month);
}
// 输出 '2025' '01',然后 '2024' '12'
// 新写法:matchAll 返回迭代器(简洁)
for (const match of str.matchAll(reg)) {
console.log(match.groups.year, match.groups.month, match.groups.day);
}
// 等价于上面,但 reg.lastIndex 自动管理,不用担心手动重置
// 也可转为数组一次性处理
var allMatches = [...str.matchAll(reg)];
console.log(allMatches.length); // 2
console.log(allMatches[0].groups.year); // '2025'
console.log(allMatches[1].groups.year); // '2024'
【代码注释】match(/g/) 全局模式只返回匹配字符串数组,丢掉捕获分组 ;matchAll(/g/) 返回每次 exec 的完整结果(含 index、groups、input)。matchAll 不改变 reg.lastIndex(内部使用副本),可安全重复调用。注意 :传入的正则必须有 g 标志,否则抛 TypeError。市面应用:批量提取 HTML 标签属性、日志文件多行数据解析、国际化文本变量替换。
【实战要点】
- 经典应用场景:表单实时验证、搜索关键词高亮、敏感词过滤、数据格式化
- 常见坑 :循环中创建RegExp导致性能问题;忘记使用
g修饰符导致只匹配第一个;全局匹配后lastIndex属性影响后续匹配 - 性能与最佳实践 :优先使用字面量方式;避免在循环中创建正则;使用具体字符类代替
.;使用锚点快速失败
全局匹配与 lastIndex 陷阱:
javascript
var reg = /a/g;
reg.test('aba'); // true,lastIndex 变为 1
reg.test('aba'); // true,从 index 1 继续
reg.test('aba'); // false,lastIndex 到末尾后重置为 0
// 循环 exec 时:同一 reg 对象不要混用 test 与 exec,或在新一轮前 reg.lastIndex = 0
【代码注释】带 g 的正则会更新 lastIndex;在 while ((m = reg.exec(str)) !== null) 中正常递增,若中途 test 打断可能导致漏匹配。动态拼接 pattern 时用 new RegExp(str, 'g') 注意双重转义 \\d。
入门示例:RegExp对象方法
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>RegExp对象方法</title>
</head>
<body>
<script>
// 1. 创建RegExp对象的三种方式
// 方式一:字面量(推荐)
var reg1 = /^\d{11}$/;
console.log(reg1.test('13800138000')); // true
// 方式二:RegExp函数
var reg2 = RegExp('^1[3-9]\\d{9}$');
console.log(reg2.test('15900159000')); // true
// 方式三:RegExp构造函数
var pattern = '^\\d{4}-\\d{2}-\\d{2}$';
var reg3 = new RegExp(pattern);
console.log(reg3.test('2025-01-15')); // true
// 2. test()方法:返回布尔值
var emailReg = /^[\w-]+@[\w-]+(\.\w+){1,2}$/;
console.log(emailReg.test('user@example.com')); // true
console.log(emailReg.test('invalid')); // false
// 3. exec()方法:返回匹配结果数组
var dateReg = /(\d{4})-(\d{2})-(\d{2})/;
var result = dateReg.exec('今天是2025-01-15');
console.log(result[0]); // '2025-01-15' 完整匹配
console.log(result[1]); // '2025' 年份
console.log(result[2]); // '01' 月份
console.log(result[3]); // '15' 日期
console.log(result.index);// 2 匹配起始位置
console.log(result.input); // 原始字符串
// 4. exec()与全局匹配
var globalReg = /\d+/g;
var str = '123abc456def789';
var match;
while ((match = globalReg.exec(str)) !== null) {
console.log(match[0]); // 依次输出 '123', '456', '789'
console.log(globalReg.lastIndex); // 更新的匹配位置
}
</script>
</body>
</html>
【代码注释】演示三种创建方式:字面量/pattern/性能最好,RegExp构造函数可用于动态模式。test()适合验证,返回布尔;exec()适合提取,返回数组含完整匹配与分组。全局匹配时需循环调用exec()直到返回null。市面应用 :表单验证用test(),数据提取用exec()。
实战示例:String对象正则方法
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>String对象正则方法</title>
</head>
<body>
<script>
var str = 'Hello World! Welcome to JavaScript.';
// 1. search():返回首个匹配索引
var index1 = str.search(/Hello/);
console.log(index1); // 0
var index2 = str.search(/world/i); // i忽略大小写
console.log(index2); // 7
var index3 = str.search(/Python/);
console.log(index3); // -1,未找到
// 2. match():返回匹配结果数组
var match1 = str.match(/JavaScript/);
console.log(match1[0]); // 'JavaScript'
console.log(match1.index); // 23
// 全局匹配
var match2 = str.match(/\b\w{5}\b/g); // 匹配5个字母的单词
console.log(match2); // ['Hello', 'World', 'Welcome']
// 3. replace():替换匹配内容
var str2 = '2025-01-15';
var newStr1 = str2.replace(/-/g, '/');
console.log(newStr1); // '2025/01/15'
// 使用$1、$2引用分组
var str3 = '张三,李四,王五';
var newStr2 = str3.replace(/(\w+),(\w+)/g, '$2和$1');
console.log(newStr2); // '李四和张三,王五'
// 使用回调函数
var str4 = 'apple orange banana';
var newStr3 = str4.replace(/\b\w+\b/g, function(word) {
return word.charAt(0).toUpperCase() + word.slice(1);
});
console.log(newStr3); // 'Apple Orange Banana'
// 4. split():按正则分割字符串
var str5 = 'apple1orange2banana3grape';
var fruits = str5.split(/\d/);
console.log(fruits); // ['apple', 'orange', 'banana', 'grape']
// 实战案例:格式化电话号码
var phone = '13800138000';
var formatted = phone.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3');
console.log(formatted); // '138-0013-8000'
// 实战案例:关键词高亮
var text = '学习JavaScript和JavaScript框架很重要';
var keyword = 'JavaScript';
var highlighted = text.replace(new RegExp(keyword, 'g'),
'<span style="color:red">' + keyword + '</span>');
console.log(highlighted);
// '学习<span style="color:red">JavaScript</span>和<span style="color:red">JavaScript</span>框架很重要'
</script>
</body>
</html>
【代码注释】search()返回索引或-1,适合判断是否存在;match()全局匹配返回所有结果的数组;replace()可使用$1引用分组或用回调函数动态处理;split()按复杂模式分割。关键词高亮是搜索功能的核心实现。市面应用:搜索结果高亮、数据格式化、模板变量替换。
【本章小结】
| 方法 | 所属对象 | 返回值 | 典型应用 |
|---|---|---|---|
| test() | RegExp | Boolean | 表单验证 |
| exec() | RegExp | Array/null | 提取数据 |
| search() | String | Number | 查找位置 |
| match() | String | Array/null | 获取所有匹配 |
| replace() | String | 新字符串 | 替换、格式化 |
| split() | String | Array | 字符串分割 |
记忆口诀:"验证用test,提取用exec,查找search,匹配match,替换replace,分割split"
【面试考点】
Q1:search()与indexOf()的区别?
A:search()支持正则表达式,可进行复杂模式匹配;indexOf()只能匹配字符串字面值。search()返回首个匹配位置,indexOf()可指定起始位置。需查找复杂模式时必须用search()。
Q2:replace()全局替换与循环replace的区别?
A:使用g修饰符的replace()一次性替换所有匹配项;循环replace()每次替换第一个,需多次调用。全局替换性能更好,代码更简洁。使用正则的$1、$2引用分组比循环处理更高效。
Q3:RegExp('^\\d+$') 与 /^\\d+$/ 何时选?
A:字面量在脚本加载时编译一次,固定规则优先;构造函数接收字符串 ,反斜杠要写成 \\d,适合用户配置规则、远程下发的 pattern。RegExp(pattern) 与 new RegExp(pattern) 在 ES5+ 等价,但勿在热路径循环里 new RegExp。
Q4:match() 全局模式与 matchAll() 有什么区别?
A:str.match(/pattern/g) 全局模式只返回匹配字符串的数组 ,丢失所有捕获分组信息;str.matchAll(/pattern/g) 返回迭代器 ,每项等价于一次 exec() 的完整结果(含 index、input、groups)。需要提取捕获分组时必须用 matchAll(ES2020+);只需要判断是否存在用 test;只需要字符串列表用 match(/g/)。matchAll 还不会改变正则的 lastIndex,可以安全地对同一正则多次调用。
三、表单验证实战
名词解释
- 表单验证:在用户提交表单前验证输入数据的合法性
- 实时验证:在用户输入过程中即时反馈验证结果
- 提交验证:在表单提交时统一验证所有字段
概念与底层原理
表单验证是正则表达式最经典的应用场景,核心流程:
#mermaid-svg-BtJ3ZDC5YbwOYerY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BtJ3ZDC5YbwOYerY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BtJ3ZDC5YbwOYerY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BtJ3ZDC5YbwOYerY .error-icon{fill:#552222;}#mermaid-svg-BtJ3ZDC5YbwOYerY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BtJ3ZDC5YbwOYerY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BtJ3ZDC5YbwOYerY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BtJ3ZDC5YbwOYerY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BtJ3ZDC5YbwOYerY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BtJ3ZDC5YbwOYerY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BtJ3ZDC5YbwOYerY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BtJ3ZDC5YbwOYerY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BtJ3ZDC5YbwOYerY .marker.cross{stroke:#333333;}#mermaid-svg-BtJ3ZDC5YbwOYerY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BtJ3ZDC5YbwOYerY p{margin:0;}#mermaid-svg-BtJ3ZDC5YbwOYerY .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-BtJ3ZDC5YbwOYerY .cluster-label text{fill:#333;}#mermaid-svg-BtJ3ZDC5YbwOYerY .cluster-label span{color:#333;}#mermaid-svg-BtJ3ZDC5YbwOYerY .cluster-label span p{background-color:transparent;}#mermaid-svg-BtJ3ZDC5YbwOYerY .label text,#mermaid-svg-BtJ3ZDC5YbwOYerY span{fill:#333;color:#333;}#mermaid-svg-BtJ3ZDC5YbwOYerY .node rect,#mermaid-svg-BtJ3ZDC5YbwOYerY .node circle,#mermaid-svg-BtJ3ZDC5YbwOYerY .node ellipse,#mermaid-svg-BtJ3ZDC5YbwOYerY .node polygon,#mermaid-svg-BtJ3ZDC5YbwOYerY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BtJ3ZDC5YbwOYerY .rough-node .label text,#mermaid-svg-BtJ3ZDC5YbwOYerY .node .label text,#mermaid-svg-BtJ3ZDC5YbwOYerY .image-shape .label,#mermaid-svg-BtJ3ZDC5YbwOYerY .icon-shape .label{text-anchor:middle;}#mermaid-svg-BtJ3ZDC5YbwOYerY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BtJ3ZDC5YbwOYerY .rough-node .label,#mermaid-svg-BtJ3ZDC5YbwOYerY .node .label,#mermaid-svg-BtJ3ZDC5YbwOYerY .image-shape .label,#mermaid-svg-BtJ3ZDC5YbwOYerY .icon-shape .label{text-align:center;}#mermaid-svg-BtJ3ZDC5YbwOYerY .node.clickable{cursor:pointer;}#mermaid-svg-BtJ3ZDC5YbwOYerY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BtJ3ZDC5YbwOYerY .arrowheadPath{fill:#333333;}#mermaid-svg-BtJ3ZDC5YbwOYerY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BtJ3ZDC5YbwOYerY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BtJ3ZDC5YbwOYerY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BtJ3ZDC5YbwOYerY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BtJ3ZDC5YbwOYerY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BtJ3ZDC5YbwOYerY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BtJ3ZDC5YbwOYerY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BtJ3ZDC5YbwOYerY .cluster text{fill:#333;}#mermaid-svg-BtJ3ZDC5YbwOYerY .cluster span{color:#333;}#mermaid-svg-BtJ3ZDC5YbwOYerY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-BtJ3ZDC5YbwOYerY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BtJ3ZDC5YbwOYerY rect.text{fill:none;stroke-width:0;}#mermaid-svg-BtJ3ZDC5YbwOYerY .icon-shape,#mermaid-svg-BtJ3ZDC5YbwOYerY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BtJ3ZDC5YbwOYerY .icon-shape p,#mermaid-svg-BtJ3ZDC5YbwOYerY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BtJ3ZDC5YbwOYerY .icon-shape .label rect,#mermaid-svg-BtJ3ZDC5YbwOYerY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BtJ3ZDC5YbwOYerY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BtJ3ZDC5YbwOYerY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BtJ3ZDC5YbwOYerY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 输入时
是
否
是
否
用户输入
实时验证
blur事件触发
正则验证
匹配成功?
显示成功提示
显示错误提示
用户提交
submit事件触发
验证所有字段
全部通过?
允许提交
阻止提交
【代码注释】流程图展示表单验证流程:实时验证在blur事件触发,提供即时反馈;提交验证统一检查所有字段,使用preventDefault()阻止无效提交。正则表达式是验证核心,通过test()方法返回布尔值判断匹配。理解此流程是构建健壮表单验证的基础。
验证策略:
- 用户名 :4-12位,由字母、数字、下划线组成
/^\w{4,12}$/ - 邮箱 :标准邮箱格式
/^[\w-]+@[\w-]+(\.\w+){1,2}$/ - 密码 :6-18位字符
/^.{6,18}$/ - 手机号 :11位数字,1开头
/^1[3-9]\d{9}$/
【实战要点】
- 经典应用场景:注册页面、登录表单、数据提交页面
- 常见坑:只在submit时验证导致用户体验差;忘记阻止默认提交行为;密码确认未与原密码比对
- 性能与最佳实践:使用blur事件验证(失去焦点时);防抖实时验证;提供清晰的错误提示;服务器端二次验证
入门示例:基础表单验证
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>基础表单验证</title>
<style>
form {
max-width: 500px;
margin: 20px auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 5px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: inline-block;
width: 80px;
text-align: right;
margin-right: 10px;
}
input {
width: 300px;
padding: 8px;
border: 1px solid #ccc;
border-radius: 3px;
}
input:focus {
outline: none;
border-color: #4CAF50;
}
.error {
color: #f44336;
font-size: 12px;
margin-left: 90px;
}
.success {
color: #4CAF50;
font-size: 12px;
margin-left: 10px;
}
button {
width: 390px;
padding: 10px;
background: #4CAF50;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
button:hover {
background: #45a049;
}
</style>
</head>
<body>
<form id="registerForm">
<div class="form-group">
<label for="username">用户名:</label>
<input type="text" id="username" placeholder="4-12位字母、数字、下划线">
<span id="usernameMsg" class="error"></span>
</div>
<div class="form-group">
<label for="email">邮箱:</label>
<input type="text" id="email" placeholder="example@domain.com">
<span id="emailMsg" class="error"></span>
</div>
<div class="form-group">
<label for="password">密码:</label>
<input type="password" id="password" placeholder="6-18位字符">
<span id="passwordMsg" class="error"></span>
</div>
<div class="form-group">
<label for="confirmPwd">确认密码:</label>
<input type="password" id="confirmPwd" placeholder="再次输入密码">
<span id="confirmPwdMsg" class="error"></span>
</div>
<button type="submit">注册</button>
</form>
<script>
(function() {
var form = document.querySelector('#registerForm');
var usernameInput = document.querySelector('#username');
var emailInput = document.querySelector('#email');
var passwordInput = document.querySelector('#password');
var confirmPwdInput = document.querySelector('#confirmPwd');
// 用户名验证
usernameInput.onblur = function() {
var value = this.value.trim();
var msgSpan = document.querySelector('#usernameMsg');
if (value === '') {
msgSpan.textContent = '用户名不能为空';
return false;
}
var reg = /^\w{4,12}$/;
if (reg.test(value)) {
msgSpan.textContent = '✓ 用户名可用';
msgSpan.className = 'success';
return true;
} else {
msgSpan.textContent = '✗ 用户名必须是4-12位字母、数字、下划线';
msgSpan.className = 'error';
return false;
}
};
// 邮箱验证
emailInput.onblur = function() {
var value = this.value.trim();
var msgSpan = document.querySelector('#emailMsg');
if (value === '') {
msgSpan.textContent = '邮箱不能为空';
return false;
}
var reg = /^[\w-]+@[\w-]+(\.\w+){1,2}$/;
if (reg.test(value)) {
msgSpan.textContent = '✓ 邮箱格式正确';
msgSpan.className = 'success';
return true;
} else {
msgSpan.textContent = '✗ 请输入正确的邮箱格式';
msgSpan.className = 'error';
return false;
}
};
// 密码验证
passwordInput.onblur = function() {
var value = this.value;
var msgSpan = document.querySelector('#passwordMsg');
if (value === '') {
msgSpan.textContent = '密码不能为空';
return false;
}
if (value.length >= 6 && value.length <= 18) {
msgSpan.textContent = '✓ 密码长度符合要求';
msgSpan.className = 'success';
return true;
} else {
msgSpan.textContent = '✗ 密码必须是6-18位';
msgSpan.className = 'error';
return false;
}
};
// 确认密码验证
confirmPwdInput.onblur = function() {
var value = this.value;
var password = passwordInput.value;
var msgSpan = document.querySelector('#confirmPwdMsg');
if (value === '') {
msgSpan.textContent = '请再次输入密码';
return false;
}
if (value === password) {
msgSpan.textContent = '✓ 密码一致';
msgSpan.className = 'success';
return true;
} else {
msgSpan.textContent = '✗ 两次密码不一致';
msgSpan.className = 'error';
return false;
}
};
// 表单提交验证
form.onsubmit = function(e) {
e.preventDefault(); // 阻止默认提交
// 触发所有字段的验证
var isUsernameValid = usernameInput.onblur();
var isEmailValid = emailInput.onblur();
var isPasswordValid = passwordInput.onblur();
var isConfirmPwdValid = confirmPwdInput.onblur();
// 所有验证通过才提交
if (isUsernameValid && isEmailValid &&
isPasswordValid && isConfirmPwdValid) {
alert('注册成功!');
// 实际项目中这里会发送Ajax请求到服务器
// fetch('/api/register', { method: 'POST', body: formData })
} else {
alert('请修正表单中的错误');
}
};
})();
</script>
</body>
</html>
【代码注释】完整表单验证流程:每个字段用blur事件触发实时验证,显示成功或错误提示;submit事件统一验证所有字段,使用preventDefault()阻止默认提交。正则表达式/^\w{4,12}$/验证用户名,邮箱正则考虑多级域名。市面应用:注册、登录、数据提交页面必备功能。
实战示例:增强验证与用户提示
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>增强表单验证</title>
<style>
.form-container {
max-width: 600px;
margin: 30px auto;
padding: 30px;
background: #f9f9f9;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h2 {
text-align: center;
color: #333;
margin-bottom: 30px;
}
.form-item {
margin-bottom: 20px;
position: relative;
}
.form-item label {
display: block;
margin-bottom: 5px;
color: #555;
font-weight: 500;
}
.form-item input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
transition: border-color 0.3s;
}
.form-item input:focus {
outline: none;
border-color: #4CAF50;
}
.form-item input.error {
border-color: #f44336;
}
.form-item input.success {
border-color: #4CAF50;
}
.error-message {
color: #f44336;
font-size: 12px;
margin-top: 5px;
display: none;
}
.strength-meter {
height: 5px;
background: #ddd;
margin-top: 5px;
border-radius: 3px;
overflow: hidden;
}
.strength-meter div {
height: 100%;
width: 0;
transition: width 0.3s, background-color 0.3s;
}
.strength-weak { background: #f44336; width: 33%; }
.strength-medium { background: #ff9800; width: 66%; }
.strength-strong { background: #4CAF50; width: 100%; }
.password-hint {
font-size: 12px;
color: #777;
margin-top: 5px;
}
button {
width: 100%;
padding: 12px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background 0.3s;
}
button:hover {
background: #45a049;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
</style>
</head>
<body>
<div class="form-container">
<h2>用户注册</h2>
<form id="registerForm">
<div class="form-item">
<label for="username">用户名</label>
<input type="text" id="username" placeholder="请输入用户名">
<div class="error-message" id="usernameError"></div>
</div>
<div class="form-item">
<label for="phone">手机号</label>
<input type="text" id="phone" placeholder="请输入手机号">
<div class="error-message" id="phoneError"></div>
</div>
<div class="form-item">
<label for="password">密码</label>
<input type="password" id="password" placeholder="请输入密码">
<div class="strength-meter">
<div id="strengthBar"></div>
</div>
<div class="password-hint">密码强度:弱-中-强</div>
<div class="error-message" id="passwordError"></div>
</div>
<button type="submit" id="submitBtn">立即注册</button>
</form>
</div>
<script>
(function() {
var form = document.querySelector('#registerForm');
var usernameInput = document.querySelector('#username');
var phoneInput = document.querySelector('#phone');
var passwordInput = document.querySelector('#password');
var submitBtn = document.querySelector('#submitBtn');
// 用户名验证
function validateUsername() {
var value = usernameInput.value.trim();
var errorDiv = document.querySelector('#usernameError');
if (value === '') {
showError(usernameInput, errorDiv, '用户名不能为空');
return false;
}
var reg = /^[a-zA-Z]\w{3,11}$/;
if (!reg.test(value)) {
showError(usernameInput, errorDiv, '用户名必须以字母开头,4-12位字母数字下划线');
return false;
}
showSuccess(usernameInput, errorDiv);
return true;
}
// 手机号验证
function validatePhone() {
var value = phoneInput.value.trim();
var errorDiv = document.querySelector('#phoneError');
if (value === '') {
showError(phoneInput, errorDiv, '手机号不能为空');
return false;
}
var reg = /^1[3-9]\d{9}$/;
if (!reg.test(value)) {
showError(phoneInput, errorDiv, '请输入正确的手机号');
return false;
}
showSuccess(phoneInput, errorDiv);
return true;
}
// 密码验证与强度检测
function validatePassword() {
var value = passwordInput.value;
var errorDiv = document.querySelector('#passwordError');
var strengthBar = document.querySelector('#strengthBar');
if (value === '') {
showError(passwordInput, errorDiv, '密码不能为空');
strengthBar.className = '';
return false;
}
if (value.length < 6 || value.length > 18) {
showError(passwordInput, errorDiv, '密码长度必须是6-18位');
strengthBar.className = '';
return false;
}
// 计算密码强度
var strength = 0;
if (/[a-z]/.test(value)) strength++;
if (/[A-Z]/.test(value)) strength++;
if (/\d/.test(value)) strength++;
if (/[^a-zA-Z0-9]/.test(value)) strength++;
strengthBar.className = '';
if (strength <= 2) {
strengthBar.classList.add('strength-weak');
} else if (strength === 3) {
strengthBar.classList.add('strength-medium');
} else {
strengthBar.classList.add('strength-strong');
}
showSuccess(passwordInput, errorDiv);
return true;
}
// 显示错误
function showError(input, errorDiv, message) {
input.classList.remove('success');
input.classList.add('error');
errorDiv.textContent = message;
errorDiv.style.display = 'block';
}
// 显示成功
function showSuccess(input, errorDiv) {
input.classList.remove('error');
input.classList.add('success');
errorDiv.style.display = 'none';
}
// 实时验证(防抖处理)
var debounceTimer;
usernameInput.addEventListener('input', function() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(validateUsername, 500);
});
phoneInput.addEventListener('input', function() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(validatePhone, 500);
});
passwordInput.addEventListener('input', function() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(validatePassword, 300);
});
// 表单提交
form.addEventListener('submit', function(e) {
e.preventDefault();
var isUsernameValid = validateUsername();
var isPhoneValid = validatePhone();
var isPasswordValid = validatePassword();
if (isUsernameValid && isPhoneValid && isPasswordValid) {
submitBtn.disabled = true;
submitBtn.textContent = '注册中...';
// 模拟Ajax请求
setTimeout(function() {
alert('注册成功!');
submitBtn.disabled = false;
submitBtn.textContent = '立即注册';
form.reset();
document.querySelectorAll('.success').forEach(function(el) {
el.classList.remove('success');
});
document.querySelector('#strengthBar').className = '';
}, 1500);
}
});
})();
</script>
</body>
</html>
【代码注释】增强版表单验证:实时验证用防抖避免频繁触发;密码强度检测通过计算字符类型数量;用户名要求以字母开头;手机号使用/^1[3-9]\d{9}$/严格匹配。CSS视觉反馈提升用户体验:错误红框、成功绿框、密码强度进度条。市面应用:现代Web应用的标准表单验证模式。
可运行示例(对比):搜索防抖
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>搜索防抖</title>
<style>input { width: 300px; padding: 10px; border: 1px solid #999; }</style>
</head>
<body>
<input type="text" id="raw" placeholder="无防抖,每次 input 都请求">
<input type="text" id="debounced" placeholder="400ms 防抖后请求">
<script>
function changeInput() {
console.log('向后端请求搜索数据...', this.value);
}
function debounce(method, delay) {
var timeId;
return function () {
var that = this, args = arguments;
clearTimeout(timeId);
timeId = setTimeout(function () { method.apply(that, args); }, delay);
};
}
document.querySelector('#raw').oninput = changeInput;
document.querySelector('#debounced').oninput = debounce(changeInput, 400);
</script>
</body>
</html>
【代码注释】apply(that, args) 保证 changeInput 里 this 为当前 input,且能拿到事件对象;与封装防抖函数配套示例一致。对比两个输入框的 Console 输出次数即可理解防抖。
验证分层建议:
| 时机 | 检查内容 | 是否防抖 |
|---|---|---|
input |
长度、非法字符 | 建议 300~500ms 防抖 |
blur |
完整格式(邮箱、手机) | 立即 test() |
submit |
全字段 + 业务规则 | 阻止默认提交 |
【代码注释】blur 做「最终格式」、input 做「轻量提示」;服务端必须二次校验,正则只做前端体验与减负。
【本章小结】
| 验证项 | 正则表达式 | 关键点 |
|---|---|---|
| 用户名 | /^[a-zA-Z]\w{3,11}$/ |
字母开头,4-12位 |
| 邮箱 | /^[\w-]+@[\w-]+(\.\w+){1,2}$/ |
标准格式 |
| 手机号 | /^1[3-9]\d{9}$/ |
1开头,11位 |
| 密码 | /^.{6,18}$/ |
长度限制 |
| URL | /^https?:\/\/.+/ |
协议开头 |
记忆口诀:"验证需求要明确,正则模式要精确,实时反馈防抖动,视觉提示体验好"
【面试考点】
Q1:实时验证的优缺点及优化方案?
A:优点是用户体验好,即时反馈;缺点是频繁触发影响性能。优化方案:使用防抖延迟验证(500ms);仅在blur时验证完整格式;输入时只做基本长度检查;缓存验证结果避免重复计算。
Q2:密码强度如何计算?
A:通过检测密码中包含的字符类型数量:小写字母、大写字母、数字、特殊符号。类型越多强度越高,每种类型占1分,总分4分。1-2分为弱,3分为中,4分为强。同时要检测长度、常见密码、连续字符等。
四、性能优化:防抖与节流
名词解释
- 防抖(Debounce):事件触发后延迟执行,期间再次触发则重新计时
- 节流(Throttle):限制函数执行频率,确保固定时间间隔执行一次
- 高频事件:触发频率高的事件,如resize、scroll、mousemove
- 事件循环(Event Loop) :JS 单线程调度机制;
setTimeout回调进入宏任务队列,等当前栈清空才执行 requestAnimationFrame(rAF):浏览器在每次重绘前调用的 API,与屏幕刷新率同步(~16.6ms/60fps)- leading / trailing edge:节流/防抖是否在触发的首次(leading)或末次(trailing)立即执行
- 宏任务(MacroTask) :
setTimeout、setInterval、事件回调等;执行一个宏任务后检查微任务队列
概念与底层原理
防抖和节流是处理高频事件的核心优化技术:
#mermaid-svg-3x8JvssjF4w5WmMi{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3x8JvssjF4w5WmMi .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3x8JvssjF4w5WmMi .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3x8JvssjF4w5WmMi .error-icon{fill:#552222;}#mermaid-svg-3x8JvssjF4w5WmMi .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3x8JvssjF4w5WmMi .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3x8JvssjF4w5WmMi .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3x8JvssjF4w5WmMi .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3x8JvssjF4w5WmMi .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3x8JvssjF4w5WmMi .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3x8JvssjF4w5WmMi .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3x8JvssjF4w5WmMi .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3x8JvssjF4w5WmMi .marker.cross{stroke:#333333;}#mermaid-svg-3x8JvssjF4w5WmMi svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3x8JvssjF4w5WmMi p{margin:0;}#mermaid-svg-3x8JvssjF4w5WmMi .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-3x8JvssjF4w5WmMi .cluster-label text{fill:#333;}#mermaid-svg-3x8JvssjF4w5WmMi .cluster-label span{color:#333;}#mermaid-svg-3x8JvssjF4w5WmMi .cluster-label span p{background-color:transparent;}#mermaid-svg-3x8JvssjF4w5WmMi .label text,#mermaid-svg-3x8JvssjF4w5WmMi span{fill:#333;color:#333;}#mermaid-svg-3x8JvssjF4w5WmMi .node rect,#mermaid-svg-3x8JvssjF4w5WmMi .node circle,#mermaid-svg-3x8JvssjF4w5WmMi .node ellipse,#mermaid-svg-3x8JvssjF4w5WmMi .node polygon,#mermaid-svg-3x8JvssjF4w5WmMi .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3x8JvssjF4w5WmMi .rough-node .label text,#mermaid-svg-3x8JvssjF4w5WmMi .node .label text,#mermaid-svg-3x8JvssjF4w5WmMi .image-shape .label,#mermaid-svg-3x8JvssjF4w5WmMi .icon-shape .label{text-anchor:middle;}#mermaid-svg-3x8JvssjF4w5WmMi .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3x8JvssjF4w5WmMi .rough-node .label,#mermaid-svg-3x8JvssjF4w5WmMi .node .label,#mermaid-svg-3x8JvssjF4w5WmMi .image-shape .label,#mermaid-svg-3x8JvssjF4w5WmMi .icon-shape .label{text-align:center;}#mermaid-svg-3x8JvssjF4w5WmMi .node.clickable{cursor:pointer;}#mermaid-svg-3x8JvssjF4w5WmMi .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3x8JvssjF4w5WmMi .arrowheadPath{fill:#333333;}#mermaid-svg-3x8JvssjF4w5WmMi .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3x8JvssjF4w5WmMi .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3x8JvssjF4w5WmMi .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3x8JvssjF4w5WmMi .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3x8JvssjF4w5WmMi .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3x8JvssjF4w5WmMi .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3x8JvssjF4w5WmMi .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3x8JvssjF4w5WmMi .cluster text{fill:#333;}#mermaid-svg-3x8JvssjF4w5WmMi .cluster span{color:#333;}#mermaid-svg-3x8JvssjF4w5WmMi div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-3x8JvssjF4w5WmMi .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3x8JvssjF4w5WmMi rect.text{fill:none;stroke-width:0;}#mermaid-svg-3x8JvssjF4w5WmMi .icon-shape,#mermaid-svg-3x8JvssjF4w5WmMi .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3x8JvssjF4w5WmMi .icon-shape p,#mermaid-svg-3x8JvssjF4w5WmMi .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3x8JvssjF4w5WmMi .icon-shape .label rect,#mermaid-svg-3x8JvssjF4w5WmMi .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3x8JvssjF4w5WmMi .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3x8JvssjF4w5WmMi .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3x8JvssjF4w5WmMi :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
用户快速触发事件
防抖处理
清除上次定时器
重新计时
延迟时间内有新触发?
执行函数
用户持续触发事件
节流处理
距离上次执行>时间间隔?
执行函数
忽略本次触发
更新上次执行时间
【代码注释】流程图对比防抖与节流机制:防抖通过清除定时器实现"最后触发执行",适合搜索场景;节流通过时间间隔判断实现"固定频率执行",适合滚动场景。两者核心区别是执行时机不同,防抖等触发结束,节流按固定节奏。理解此差异是选择正确优化方案的关键。
防抖原理:每次触发清除旧定时器,创建新定时器,只有停止触发一段时间后才执行。适合"最后一次触发后执行"的场景。
节流原理:记录上次执行时间,每次触发检查是否超过间隔,超过则执行并更新时间。适合"固定频率执行"的场景。
深化:事件循环与防抖的关系
理解 setTimeout 防抖为什么有效,需要从事件循环(Event Loop)出发:
JS 引擎主线程 宏任务队列 用户(频繁输入) JS 引擎主线程 宏任务队列 用户(频繁输入) #mermaid-svg-OruB9vEbEipMA12d{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-OruB9vEbEipMA12d .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OruB9vEbEipMA12d .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OruB9vEbEipMA12d .error-icon{fill:#552222;}#mermaid-svg-OruB9vEbEipMA12d .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OruB9vEbEipMA12d .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OruB9vEbEipMA12d .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OruB9vEbEipMA12d .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OruB9vEbEipMA12d .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OruB9vEbEipMA12d .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OruB9vEbEipMA12d .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OruB9vEbEipMA12d .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OruB9vEbEipMA12d .marker.cross{stroke:#333333;}#mermaid-svg-OruB9vEbEipMA12d svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OruB9vEbEipMA12d p{margin:0;}#mermaid-svg-OruB9vEbEipMA12d .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-OruB9vEbEipMA12d text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-OruB9vEbEipMA12d .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-OruB9vEbEipMA12d .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-OruB9vEbEipMA12d .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-OruB9vEbEipMA12d .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-OruB9vEbEipMA12d #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-OruB9vEbEipMA12d .sequenceNumber{fill:white;}#mermaid-svg-OruB9vEbEipMA12d #sequencenumber{fill:#333;}#mermaid-svg-OruB9vEbEipMA12d #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-OruB9vEbEipMA12d .messageText{fill:#333;stroke:none;}#mermaid-svg-OruB9vEbEipMA12d .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-OruB9vEbEipMA12d .labelText,#mermaid-svg-OruB9vEbEipMA12d .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-OruB9vEbEipMA12d .loopText,#mermaid-svg-OruB9vEbEipMA12d .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-OruB9vEbEipMA12d .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-OruB9vEbEipMA12d .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-OruB9vEbEipMA12d .noteText,#mermaid-svg-OruB9vEbEipMA12d .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-OruB9vEbEipMA12d .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-OruB9vEbEipMA12d .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-OruB9vEbEipMA12d .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-OruB9vEbEipMA12d .actorPopupMenu{position:absolute;}#mermaid-svg-OruB9vEbEipMA12d .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-OruB9vEbEipMA12d .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-OruB9vEbEipMA12d .actor-man circle,#mermaid-svg-OruB9vEbEipMA12d line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-OruB9vEbEipMA12d :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户停止输入 input 事件触发 clearTimeout(旧定时器) setTimeout(cb, 400ms) 200ms 后再次触发 clearTimeout(旧定时器) ← 取消上一个 setTimeout(cb, 400ms) ← 重新计时 400ms 后定时器到期 cb() 进入宏任务队列 执行 cb(搜索/验证)
【代码注释】setTimeout 回调进入宏任务队列 ,只有当前执行栈清空后才会被取出执行。用户连续输入期间,每次 clearTimeout 取消上一个宏任务,setTimeout 注册新宏任务;停止输入后等待 400ms 无新触发,定时器到期,cb 才真正执行。防抖的本质是:让同一时间段只有一个宏任务进入队列。
深化:requestAnimationFrame 节流(动画场景专用)
setTimeout 节流的间隔是固定毫秒数,但浏览器绘制帧率(通常 60fps ≈ 16.6ms)是动态的。对于视觉动画、拖拽、画布绘制 ,应改用 requestAnimationFrame 节流:
javascript
// rAF 节流:与浏览器刷新率同步,避免多余渲染
function throttleRAF(fn) {
var rafId = null;
return function() {
var context = this;
var args = arguments;
// 如果已有 rAF 在等待,不重复注册
if (rafId !== null) return;
rafId = requestAnimationFrame(function() {
fn.apply(context, args);
rafId = null; // 执行后开放下一帧注册
});
};
}
// 用法:拖拽位置更新(每帧最多执行一次)
var card = document.querySelector('.drag-card');
document.addEventListener('mousemove', throttleRAF(function(e) {
card.style.transform = 'translate(' + e.clientX + 'px, ' + e.clientY + 'px)';
}));
【代码注释】requestAnimationFrame 在浏览器下次重绘前调用回调,自动与屏幕刷新率对齐(60Hz → 16.6ms,120Hz → 8.3ms)。rAF 节流的优势:① 不多渲染(帧间只取最新状态);② 不少渲染(每帧都更新);③ 页面不可见时自动暂停(省 CPU)。对比 :setTimeout(200ms) 节流在 60fps 屏下会漏帧(200ms 约 12 帧才执行一次);rAF 节流每帧一次,动画最流畅。市面应用:Canvas 绘图、拖拽排序(Sortable.js 内部)、实时数据图表(ECharts resize)。
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 搜索输入联想 | debounce(400ms) |
等用户停止输入 |
| scroll 触底加载 | throttle(200ms) 时间戳版 |
持续执行,不漏掉触底 |
| 拖拽/canvas 动画 | throttleRAF |
与帧率同步,最流畅 |
| 窗口 resize 重算布局 | debounce(200ms) |
只关心最终尺寸 |
| 按钮防重复提交 | debounce(immediate=true) |
首次立即,后续冷却 |
防抖与节流封装(可直接复用):
javascript
function debounce(method, delay) {
var timeId;
return function () {
var that = this;
var args = arguments;
clearTimeout(timeId);
timeId = setTimeout(function () {
method.apply(that, args);
}, delay);
};
}
function throttle(method, delay) {
var prev = Date.now();
return function () {
var that = this;
var args = arguments;
var now = Date.now();
if (now - prev >= delay) {
method.apply(that, args);
prev = now;
}
};
}
【代码注释】防抖用 clearTimeout + setTimeout;节流用时间戳判断间隔。搜索联想、resize、防重复提交 → 防抖;拖拽、滚动加载 → 节流。
防抖 vs 节流(选型表):
| 维度 | 防抖 debounce | 节流 throttle |
|---|---|---|
| 触发持续时 | 不断重置计时,停下来的那次才执行 | 每隔固定时间最多执行一次 |
| 典型场景 | 搜索联想、resize 结束布局、表单 input 校验 | 滚动触底、mousemove、缩略图箭头(timeStamp) |
| 比喻 | 电梯等人:有人进就重新关门倒计时 | 地铁发车:到点就走,不等所有人到齐 |
【实战要点】
- 经典应用场景:防抖用于搜索输入、窗口resize;节流用于滚动加载、鼠标移动
- 常见坑 :忘记使用
apply传递正确的this和arguments;定时器未清理导致内存泄漏;节流时间间隔设置不合理 - 性能与最佳实践 :使用
requestAnimationFrame优化动画;时间戳方式比定时器性能更好;结合两者优点实现增强版节流;考虑首尾是否执行的需求
入门示例:防抖与节流对比
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>防抖与节流对比</title>
<style>
.container {
display: flex;
gap: 20px;
padding: 20px;
}
.box {
flex: 1;
height: 300px;
border: 2px solid #ddd;
padding: 10px;
font-family: monospace;
font-size: 12px;
overflow-y: auto;
background: #f9f9f9;
}
.box h3 {
margin-top: 0;
text-align: center;
}
input {
width: 100%;
padding: 8px;
margin: 10px 0;
border: 1px solid #ddd;
}
.scroll-area {
height: 200px;
overflow-y: scroll;
border: 1px solid #ddd;
background: white;
}
.scroll-content {
height: 1000px;
background: linear-gradient(to bottom, #f0f0f0, #e0e0e0);
display: flex;
align-items: center;
justify-content: center;
}
</style>
</head>
<body>
<div class="container">
<div class="box">
<h3>无优化(立即执行)</h3>
<input type="text" id="input1" placeholder="输入文字...">
<div id="log1"></div>
</div>
<div class="box">
<h3>防抖(停止输入后执行)</h3>
<input type="text" id="input2" placeholder="输入文字...">
<div id="log2"></div>
</div>
<div class="box">
<h3>节流(固定频率执行)</h3>
<div class="scroll-area" id="scrollArea">
<div class="scroll-content">滚动此区域</div>
</div>
<div id="log3"></div>
</div>
</div>
<script>
(function() {
var input1 = document.querySelector('#input1');
var input2 = document.querySelector('#input2');
var scrollArea = document.querySelector('#scrollArea');
var log1 = document.querySelector('#log1');
var log2 = document.querySelector('#log2');
var log3 = document.querySelector('#log3');
var count1 = 0;
var count2 = 0;
var count3 = 0;
// 无优化:每次输入立即执行
input1.addEventListener('input', function(e) {
count1++;
log1.innerHTML = '执行次数:' + count1 + '<br>值:' + e.target.value;
});
// 防抖:停止输入500ms后执行
input2.addEventListener('input', debounce(function(e) {
count2++;
log2.innerHTML = '执行次数:' + count2 + '<br>值:' + e.target.value;
}, 500));
// 节流:滚动时每200ms执行一次
scrollArea.addEventListener('scroll', throttle(function(e) {
count3++;
log3.innerHTML = '执行次数:' + count3 + '<br>scrollTop:' + e.target.scrollTop;
}, 200));
// 防抖函数封装
function debounce(func, delay) {
var timer = null;
return function() {
var context = this;
var args = arguments;
clearTimeout(timer);
timer = setTimeout(function() {
func.apply(context, args);
}, delay);
};
}
// 节流函数封装(时间戳方式)
function throttle(func, delay) {
var lastTime = 0;
return function() {
var now = Date.now();
var context = this;
var args = arguments;
if (now - lastTime >= delay) {
lastTime = now;
func.apply(context, args);
}
};
}
})();
</script>
</body>
</html>
【代码注释】对比三种处理方式:无优化每次触发都执行(高频);防抖停止触发后延迟执行(搜索场景);节流固定频率执行(滚动场景)。防抖通过clearTimeout不断重置计时器;节流通过时间戳判断间隔。理解两者的区别是选择正确优化方案的关键。市面应用:百度搜索用防抖,懒加载用节流。
实战示例:完整防抖节流实现
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>完整防抖节流实现</title>
<style>
.demo-container {
max-width: 800px;
margin: 20px auto;
padding: 20px;
}
.section {
margin-bottom: 30px;
padding: 15px;
background: #f5f5f5;
border-radius: 5px;
}
.section h3 {
margin-top: 0;
color: #333;
}
.search-box {
position: relative;
width: 100%;
max-width: 400px;
}
.search-box input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.search-results {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #ddd;
border-top: none;
display: none;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.search-results.show {
display: block;
}
.search-results div {
padding: 10px;
cursor: pointer;
border-bottom: 1px solid #eee;
}
.search-results div:hover {
background: #f0f0f0;
}
.scroll-box {
height: 300px;
overflow-y: auto;
border: 1px solid #ddd;
background: white;
padding: 15px;
}
.scroll-content {
height: 2000px;
background: linear-gradient(to bottom, #f8f8f8 0%, #e8e8e8 100%);
}
.info-panel {
position: fixed;
top: 20px;
right: 20px;
padding: 15px;
background: rgba(0, 0, 0, 0.8);
color: white;
border-radius: 5px;
font-size: 12px;
z-index: 1000;
}
.btn {
padding: 8px 16px;
margin: 5px;
border: none;
border-radius: 4px;
cursor: pointer;
background: #4CAF50;
color: white;
}
.btn:hover {
background: #45a049;
}
.log {
font-family: monospace;
font-size: 11px;
max-height: 200px;
overflow-y: auto;
background: #222;
color: #0f0;
padding: 10px;
border-radius: 4px;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="demo-container">
<!-- 搜索防抖示例 -->
<div class="section">
<h3>搜索防抖示例</h3>
<div class="search-box">
<input type="text" id="searchInput" placeholder="输入关键词(停止输入500ms后搜索)">
<div class="search-results" id="searchResults"></div>
</div>
<div class="log" id="searchLog"></div>
</div>
<!-- 滚动节流示例 -->
<div class="section">
<h3>滚动节流示例</h3>
<div class="scroll-box" id="scrollBox">
<div class="scroll-content">向下滚动查看节流效果</div>
</div>
<div class="log" id="scrollLog"></div>
</div>
<!-- 按钮防抖示例 -->
<div class="section">
<h3>按钮防抖示例(防止重复提交)</h3>
<button class="btn" id="submitBtn">提交(点击查看效果)</button>
<div class="log" id="buttonLog"></div>
</div>
</div>
<div class="info-panel">
<div>鼠标位置:<span id="mousePos">0, 0</span></div>
<div>处理次数:<span id="processCount">0</span></div>
</div>
<script>
(function() {
// ==================== 防抖函数 ====================
/**
* 防抖函数
* @param {Function} func 要执行的函数
* @param {number} delay 延迟时间(ms)
* @param {boolean} immediate 是否立即执行
* @return {Function} 防抖后的函数
*/
function debounce(func, delay, immediate) {
var timer = null;
return function() {
var context = this;
var args = arguments;
var callNow = immediate && !timer;
clearTimeout(timer);
timer = setTimeout(function() {
timer = null;
if (!immediate) {
func.apply(context, args);
}
}, delay);
if (callNow) {
func.apply(context, args);
}
};
}
// ==================== 节流函数 ====================
/**
* 节流函数(时间戳 + 定时器混合方式)
* @param {Function} func 要执行的函数
* @param {number} delay 时间间隔(ms)
* @param {Object} options 配置选项
* @return {Function} 节流后的函数
*/
function throttle(func, delay, options) {
var timer = null;
var lastTime = 0;
var remaining = 0;
options = options || {};
var leading = options.leading !== false; // 首次是否执行
var trailing = options.trailing !== false; // 结尾是否执行
return function() {
var context = this;
var args = arguments;
var now = Date.now();
// 首次不执行
if (!leading && !lastTime) {
lastTime = now;
}
// 计算剩余时间
remaining = delay - (now - lastTime);
if (remaining <= 0 || remaining > delay) {
if (timer) {
clearTimeout(timer);
timer = null;
}
lastTime = now;
func.apply(context, args);
} else if (!timer && trailing) {
// 结尾执行
timer = setTimeout(function() {
lastTime = leading ? Date.now() : 0;
timer = null;
func.apply(context, args);
}, remaining);
}
};
}
// ==================== 搜索防抖示例 ====================
var searchInput = document.querySelector('#searchInput');
var searchResults = document.querySelector('#searchResults');
var searchLog = document.querySelector('#searchLog');
// 模拟搜索数据
var searchData = [
'JavaScript教程', 'Java教程', 'Python教程',
'JavaScript框架', '正则表达式', '前端开发',
'后端开发', '全栈开发', '移动开发'
];
var debouncedSearch = debounce(function(keyword) {
addLog(searchLog, '执行搜索:' + keyword);
// 模拟搜索
var results = searchData.filter(function(item) {
return item.toLowerCase().includes(keyword.toLowerCase());
});
// 显示结果
searchResults.innerHTML = '';
if (results.length > 0) {
searchResults.classList.add('show');
results.forEach(function(item) {
var div = document.createElement('div');
div.textContent = item;
div.onclick = function() {
searchInput.value = item;
searchResults.classList.remove('show');
};
searchResults.appendChild(div);
});
} else {
searchResults.classList.remove('show');
}
}, 500);
searchInput.addEventListener('input', function(e) {
var keyword = e.target.value.trim();
addLog(searchLog, '输入:' + keyword);
if (keyword) {
debouncedSearch(keyword);
} else {
searchResults.classList.remove('show');
}
});
// 点击外部关闭搜索结果
document.addEventListener('click', function(e) {
if (!e.target.closest('.search-box')) {
searchResults.classList.remove('show');
}
});
// ==================== 滚动节流示例 ====================
var scrollBox = document.querySelector('#scrollBox');
var scrollLog = document.querySelector('#scrollLog');
var throttledScroll = throttle(function(scrollTop) {
addLog(scrollLog, '滚动位置:' + scrollTop);
}, 100);
scrollBox.addEventListener('scroll', function(e) {
throttledScroll(e.target.scrollTop);
});
// ==================== 按钮防抖示例 ====================
var submitBtn = document.querySelector('#submitBtn');
var buttonLog = document.querySelector('#buttonLog');
var debouncedSubmit = debounce(function() {
addLog(buttonLog, '提交成功!');
// 模拟Ajax请求
setTimeout(function() {
submitBtn.disabled = false;
submitBtn.textContent = '提交';
}, 2000);
}, 1000, true); // immediate=true,立即执行
submitBtn.addEventListener('click', function() {
this.disabled = true;
this.textContent = '提交中...';
addLog(buttonLog, '点击按钮');
debouncedSubmit();
});
// ==================== 鼠标移动节流示例 ====================
var mousePos = document.querySelector('#mousePos');
var processCount = document.querySelector('#processCount');
var count = 0;
var throttledMouseMove = throttle(function(x, y) {
count++;
processCount.textContent = count;
}, 50);
document.addEventListener('mousemove', function(e) {
mousePos.textContent = e.clientX + ', ' + e.clientY;
throttledMouseMove(e.clientX, e.clientY);
});
// ==================== 辅助函数 ====================
function addLog(container, message) {
var time = new Date().toLocaleTimeString();
var log = document.createElement('div');
log.textContent = '[' + time + '] ' + message;
container.insertBefore(log, container.firstChild);
// 限制日志数量
while (container.children.length > 10) {
container.removeChild(container.lastChild);
}
}
})();
</script>
</body>
</html>
【代码注释】完整防抖节流实现:防抖支持立即执行immediate参数;节流混合时间戳与定时器,支持首尾执行配置。搜索用防抖减少请求;滚动用节流优化性能;按钮用防抖防止重复提交;鼠标移动用节流降低频率。理解两者差异是性能优化的核心。市面应用:搜索框、无限滚动、按钮点击、窗口resize。
【本章小结】
| 技术 | 原理 | 适用场景 | 典型应用 |
|---|---|---|---|
| 防抖 | 停止触发后延迟执行 | 搜索、resize、表单验证 | 搜索输入提示 |
| 节流 | 固定频率执行 | 滚动、mousemove | 懒加载、动画 |
| 混合 | 防抖+节流结合 | 复杂高频事件 | 窗口resize |
记忆口诀:"防抖适合搜索场景,节流适合滚动页面,混合使用效果最佳,性能优化用户体验"
【面试考点】
Q1:防抖与节流的区别及选择?
A:防抖是停止触发后执行,适合搜索框输入;节流是固定频率执行,适合滚动加载。选择依据:搜索、验证用防抖(只关心最后结果);滚动、动画用节流(持续处理)。实时性要求高用节流,减少请求用防抖。
Q2:如何实现立即执行的防抖?
A:增加immediate参数,首次触发立即执行,后续触发防抖。实现时用标志位判断是否首次,定时器结束时重置。callNow = immediate && !timer确保首次执行,clearTimeout(timer)清除旧定时器,setTimeout延迟执行后续调用。
Q3:节流除了时间戳,还能用定时器吗?
A:可以。时间戳版在间隔内忽略 多余触发(首次立即执行,末次可能丢失);定时器版在间隔内合并 为一次尾调用(首次延迟,末次保证执行)。高级版把两者混合:时间戳版处理首次立即,定时器版保证末次执行(类似 Lodash 的 {leading: true, trailing: true})。
Q4:防抖为什么能减少请求次数?与事件循环有什么关系?
A:防抖利用 setTimeout 将回调放入宏任务队列 。用户连续输入时,每次 clearTimeout 取消上一个未执行的宏任务,只有停止输入超过 delay ms 后,才有一个宏任务真正进入执行栈并发送请求。事件循环保证"当前栈清空后才取下一宏任务",因此 clearTimeout 能在回调执行前有效取消它。
Q5:requestAnimationFrame 节流与 setTimeout 节流有何区别?
A:setTimeout(fn, 16) 固定 16ms 间隔,但 JS 定时器精度有限且不保证与浏览器绘制帧对齐;requestAnimationFrame 由浏览器在每次重绘前调用,自动与屏幕刷新率同步(60Hz 约 16.6ms,120Hz 约 8.3ms)。对视觉动画、拖拽、canvas ,rAF 节流更准确;页面不可见时 rAF 自动暂停省 CPU。对API 请求限速、非视觉逻辑 ,setTimeout 节流更合适(不依赖屏幕刷新)。
可运行示例(补充):定时器版节流
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>定时器节流</title>
<style>
#box { height: 120px; overflow: auto; border: 1px solid #ccc; }
#inner { height: 800px; background: linear-gradient(#eee,#ccc); }
</style>
</head>
<body>
<div id="box"><div id="inner">滚动</div></div>
<p id="log">执行次数:0</p>
<script>
var n = 0, log = document.getElementById('log');
function throttleTimer(fn, delay) {
var timer = null;
return function () {
var ctx = this, args = arguments;
if (!timer) {
timer = setTimeout(function () {
fn.apply(ctx, args);
timer = null;
}, delay);
}
};
}
document.getElementById('box').onscroll = throttleTimer(function () {
n++;
log.textContent = '执行次数:' + n + ' scrollTop=' + this.scrollTop;
}, 200);
</script>
</body>
</html>
【代码注释】滚动过程中每 200ms 最多触发一次回调(尾执行);与时间戳版对比 Console 次数。电商缩略图箭头用 timeStamp 节流是同一类「限频」需求。
总结
知识点回顾
#mermaid-svg-PjHJBQXcrWSskyDH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PjHJBQXcrWSskyDH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PjHJBQXcrWSskyDH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PjHJBQXcrWSskyDH .error-icon{fill:#552222;}#mermaid-svg-PjHJBQXcrWSskyDH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PjHJBQXcrWSskyDH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PjHJBQXcrWSskyDH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PjHJBQXcrWSskyDH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PjHJBQXcrWSskyDH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PjHJBQXcrWSskyDH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PjHJBQXcrWSskyDH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PjHJBQXcrWSskyDH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PjHJBQXcrWSskyDH .marker.cross{stroke:#333333;}#mermaid-svg-PjHJBQXcrWSskyDH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PjHJBQXcrWSskyDH p{margin:0;}#mermaid-svg-PjHJBQXcrWSskyDH .edge{stroke-width:3;}#mermaid-svg-PjHJBQXcrWSskyDH .section--1 rect,#mermaid-svg-PjHJBQXcrWSskyDH .section--1 path,#mermaid-svg-PjHJBQXcrWSskyDH .section--1 circle,#mermaid-svg-PjHJBQXcrWSskyDH .section--1 polygon,#mermaid-svg-PjHJBQXcrWSskyDH .section--1 path{fill:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-PjHJBQXcrWSskyDH .section--1 text{fill:#ffffff;}#mermaid-svg-PjHJBQXcrWSskyDH .node-icon--1{font-size:40px;color:#ffffff;}#mermaid-svg-PjHJBQXcrWSskyDH .section-edge--1{stroke:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-PjHJBQXcrWSskyDH .edge-depth--1{stroke-width:17;}#mermaid-svg-PjHJBQXcrWSskyDH .section--1 line{stroke:hsl(60, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-PjHJBQXcrWSskyDH .disabled,#mermaid-svg-PjHJBQXcrWSskyDH .disabled circle,#mermaid-svg-PjHJBQXcrWSskyDH .disabled text{fill:lightgray;}#mermaid-svg-PjHJBQXcrWSskyDH .disabled text{fill:#efefef;}#mermaid-svg-PjHJBQXcrWSskyDH .section-0 rect,#mermaid-svg-PjHJBQXcrWSskyDH .section-0 path,#mermaid-svg-PjHJBQXcrWSskyDH .section-0 circle,#mermaid-svg-PjHJBQXcrWSskyDH .section-0 polygon,#mermaid-svg-PjHJBQXcrWSskyDH .section-0 path{fill:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-PjHJBQXcrWSskyDH .section-0 text{fill:black;}#mermaid-svg-PjHJBQXcrWSskyDH .node-icon-0{font-size:40px;color:black;}#mermaid-svg-PjHJBQXcrWSskyDH .section-edge-0{stroke:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-PjHJBQXcrWSskyDH .edge-depth-0{stroke-width:14;}#mermaid-svg-PjHJBQXcrWSskyDH .section-0 line{stroke:hsl(240, 100%, 83.5294117647%);stroke-width:3;}#mermaid-svg-PjHJBQXcrWSskyDH .disabled,#mermaid-svg-PjHJBQXcrWSskyDH .disabled circle,#mermaid-svg-PjHJBQXcrWSskyDH .disabled text{fill:lightgray;}#mermaid-svg-PjHJBQXcrWSskyDH .disabled text{fill:#efefef;}#mermaid-svg-PjHJBQXcrWSskyDH .section-1 rect,#mermaid-svg-PjHJBQXcrWSskyDH .section-1 path,#mermaid-svg-PjHJBQXcrWSskyDH .section-1 circle,#mermaid-svg-PjHJBQXcrWSskyDH .section-1 polygon,#mermaid-svg-PjHJBQXcrWSskyDH .section-1 path{fill:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-PjHJBQXcrWSskyDH .section-1 text{fill:black;}#mermaid-svg-PjHJBQXcrWSskyDH .node-icon-1{font-size:40px;color:black;}#mermaid-svg-PjHJBQXcrWSskyDH .section-edge-1{stroke:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-PjHJBQXcrWSskyDH .edge-depth-1{stroke-width:11;}#mermaid-svg-PjHJBQXcrWSskyDH .section-1 line{stroke:hsl(260, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-PjHJBQXcrWSskyDH .disabled,#mermaid-svg-PjHJBQXcrWSskyDH .disabled circle,#mermaid-svg-PjHJBQXcrWSskyDH .disabled text{fill:lightgray;}#mermaid-svg-PjHJBQXcrWSskyDH .disabled text{fill:#efefef;}#mermaid-svg-PjHJBQXcrWSskyDH .section-2 rect,#mermaid-svg-PjHJBQXcrWSskyDH .section-2 path,#mermaid-svg-PjHJBQXcrWSskyDH .section-2 circle,#mermaid-svg-PjHJBQXcrWSskyDH .section-2 polygon,#mermaid-svg-PjHJBQXcrWSskyDH .section-2 path{fill:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-PjHJBQXcrWSskyDH .section-2 text{fill:#ffffff;}#mermaid-svg-PjHJBQXcrWSskyDH .node-icon-2{font-size:40px;color:#ffffff;}#mermaid-svg-PjHJBQXcrWSskyDH .section-edge-2{stroke:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-PjHJBQXcrWSskyDH .edge-depth-2{stroke-width:8;}#mermaid-svg-PjHJBQXcrWSskyDH .section-2 line{stroke:hsl(90, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-PjHJBQXcrWSskyDH .disabled,#mermaid-svg-PjHJBQXcrWSskyDH .disabled circle,#mermaid-svg-PjHJBQXcrWSskyDH .disabled text{fill:lightgray;}#mermaid-svg-PjHJBQXcrWSskyDH .disabled text{fill:#efefef;}#mermaid-svg-PjHJBQXcrWSskyDH .section-3 rect,#mermaid-svg-PjHJBQXcrWSskyDH .section-3 path,#mermaid-svg-PjHJBQXcrWSskyDH .section-3 circle,#mermaid-svg-PjHJBQXcrWSskyDH .section-3 polygon,#mermaid-svg-PjHJBQXcrWSskyDH .section-3 path{fill:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-PjHJBQXcrWSskyDH .section-3 text{fill:black;}#mermaid-svg-PjHJBQXcrWSskyDH .node-icon-3{font-size:40px;color:black;}#mermaid-svg-PjHJBQXcrWSskyDH .section-edge-3{stroke:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-PjHJBQXcrWSskyDH .edge-depth-3{stroke-width:5;}#mermaid-svg-PjHJBQXcrWSskyDH .section-3 line{stroke:hsl(120, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-PjHJBQXcrWSskyDH .disabled,#mermaid-svg-PjHJBQXcrWSskyDH .disabled circle,#mermaid-svg-PjHJBQXcrWSskyDH .disabled text{fill:lightgray;}#mermaid-svg-PjHJBQXcrWSskyDH .disabled text{fill:#efefef;}#mermaid-svg-PjHJBQXcrWSskyDH .section-4 rect,#mermaid-svg-PjHJBQXcrWSskyDH .section-4 path,#mermaid-svg-PjHJBQXcrWSskyDH .section-4 circle,#mermaid-svg-PjHJBQXcrWSskyDH .section-4 polygon,#mermaid-svg-PjHJBQXcrWSskyDH .section-4 path{fill:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-PjHJBQXcrWSskyDH .section-4 text{fill:black;}#mermaid-svg-PjHJBQXcrWSskyDH .node-icon-4{font-size:40px;color:black;}#mermaid-svg-PjHJBQXcrWSskyDH .section-edge-4{stroke:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-PjHJBQXcrWSskyDH .edge-depth-4{stroke-width:2;}#mermaid-svg-PjHJBQXcrWSskyDH .section-4 line{stroke:hsl(150, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-PjHJBQXcrWSskyDH .disabled,#mermaid-svg-PjHJBQXcrWSskyDH .disabled circle,#mermaid-svg-PjHJBQXcrWSskyDH .disabled text{fill:lightgray;}#mermaid-svg-PjHJBQXcrWSskyDH .disabled text{fill:#efefef;}#mermaid-svg-PjHJBQXcrWSskyDH .section-5 rect,#mermaid-svg-PjHJBQXcrWSskyDH .section-5 path,#mermaid-svg-PjHJBQXcrWSskyDH .section-5 circle,#mermaid-svg-PjHJBQXcrWSskyDH .section-5 polygon,#mermaid-svg-PjHJBQXcrWSskyDH .section-5 path{fill:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-PjHJBQXcrWSskyDH .section-5 text{fill:black;}#mermaid-svg-PjHJBQXcrWSskyDH .node-icon-5{font-size:40px;color:black;}#mermaid-svg-PjHJBQXcrWSskyDH .section-edge-5{stroke:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-PjHJBQXcrWSskyDH .edge-depth-5{stroke-width:-1;}#mermaid-svg-PjHJBQXcrWSskyDH .section-5 line{stroke:hsl(180, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-PjHJBQXcrWSskyDH .disabled,#mermaid-svg-PjHJBQXcrWSskyDH .disabled circle,#mermaid-svg-PjHJBQXcrWSskyDH .disabled text{fill:lightgray;}#mermaid-svg-PjHJBQXcrWSskyDH .disabled text{fill:#efefef;}#mermaid-svg-PjHJBQXcrWSskyDH .section-6 rect,#mermaid-svg-PjHJBQXcrWSskyDH .section-6 path,#mermaid-svg-PjHJBQXcrWSskyDH .section-6 circle,#mermaid-svg-PjHJBQXcrWSskyDH .section-6 polygon,#mermaid-svg-PjHJBQXcrWSskyDH .section-6 path{fill:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-PjHJBQXcrWSskyDH .section-6 text{fill:black;}#mermaid-svg-PjHJBQXcrWSskyDH .node-icon-6{font-size:40px;color:black;}#mermaid-svg-PjHJBQXcrWSskyDH .section-edge-6{stroke:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-PjHJBQXcrWSskyDH .edge-depth-6{stroke-width:-4;}#mermaid-svg-PjHJBQXcrWSskyDH .section-6 line{stroke:hsl(210, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-PjHJBQXcrWSskyDH .disabled,#mermaid-svg-PjHJBQXcrWSskyDH .disabled circle,#mermaid-svg-PjHJBQXcrWSskyDH .disabled text{fill:lightgray;}#mermaid-svg-PjHJBQXcrWSskyDH .disabled text{fill:#efefef;}#mermaid-svg-PjHJBQXcrWSskyDH .section-7 rect,#mermaid-svg-PjHJBQXcrWSskyDH .section-7 path,#mermaid-svg-PjHJBQXcrWSskyDH .section-7 circle,#mermaid-svg-PjHJBQXcrWSskyDH .section-7 polygon,#mermaid-svg-PjHJBQXcrWSskyDH .section-7 path{fill:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-PjHJBQXcrWSskyDH .section-7 text{fill:black;}#mermaid-svg-PjHJBQXcrWSskyDH .node-icon-7{font-size:40px;color:black;}#mermaid-svg-PjHJBQXcrWSskyDH .section-edge-7{stroke:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-PjHJBQXcrWSskyDH .edge-depth-7{stroke-width:-7;}#mermaid-svg-PjHJBQXcrWSskyDH .section-7 line{stroke:hsl(270, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-PjHJBQXcrWSskyDH .disabled,#mermaid-svg-PjHJBQXcrWSskyDH .disabled circle,#mermaid-svg-PjHJBQXcrWSskyDH .disabled text{fill:lightgray;}#mermaid-svg-PjHJBQXcrWSskyDH .disabled text{fill:#efefef;}#mermaid-svg-PjHJBQXcrWSskyDH .section-8 rect,#mermaid-svg-PjHJBQXcrWSskyDH .section-8 path,#mermaid-svg-PjHJBQXcrWSskyDH .section-8 circle,#mermaid-svg-PjHJBQXcrWSskyDH .section-8 polygon,#mermaid-svg-PjHJBQXcrWSskyDH .section-8 path{fill:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-PjHJBQXcrWSskyDH .section-8 text{fill:black;}#mermaid-svg-PjHJBQXcrWSskyDH .node-icon-8{font-size:40px;color:black;}#mermaid-svg-PjHJBQXcrWSskyDH .section-edge-8{stroke:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-PjHJBQXcrWSskyDH .edge-depth-8{stroke-width:-10;}#mermaid-svg-PjHJBQXcrWSskyDH .section-8 line{stroke:hsl(330, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-PjHJBQXcrWSskyDH .disabled,#mermaid-svg-PjHJBQXcrWSskyDH .disabled circle,#mermaid-svg-PjHJBQXcrWSskyDH .disabled text{fill:lightgray;}#mermaid-svg-PjHJBQXcrWSskyDH .disabled text{fill:#efefef;}#mermaid-svg-PjHJBQXcrWSskyDH .section-9 rect,#mermaid-svg-PjHJBQXcrWSskyDH .section-9 path,#mermaid-svg-PjHJBQXcrWSskyDH .section-9 circle,#mermaid-svg-PjHJBQXcrWSskyDH .section-9 polygon,#mermaid-svg-PjHJBQXcrWSskyDH .section-9 path{fill:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-PjHJBQXcrWSskyDH .section-9 text{fill:black;}#mermaid-svg-PjHJBQXcrWSskyDH .node-icon-9{font-size:40px;color:black;}#mermaid-svg-PjHJBQXcrWSskyDH .section-edge-9{stroke:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-PjHJBQXcrWSskyDH .edge-depth-9{stroke-width:-13;}#mermaid-svg-PjHJBQXcrWSskyDH .section-9 line{stroke:hsl(0, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-PjHJBQXcrWSskyDH .disabled,#mermaid-svg-PjHJBQXcrWSskyDH .disabled circle,#mermaid-svg-PjHJBQXcrWSskyDH .disabled text{fill:lightgray;}#mermaid-svg-PjHJBQXcrWSskyDH .disabled text{fill:#efefef;}#mermaid-svg-PjHJBQXcrWSskyDH .section-10 rect,#mermaid-svg-PjHJBQXcrWSskyDH .section-10 path,#mermaid-svg-PjHJBQXcrWSskyDH .section-10 circle,#mermaid-svg-PjHJBQXcrWSskyDH .section-10 polygon,#mermaid-svg-PjHJBQXcrWSskyDH .section-10 path{fill:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-PjHJBQXcrWSskyDH .section-10 text{fill:black;}#mermaid-svg-PjHJBQXcrWSskyDH .node-icon-10{font-size:40px;color:black;}#mermaid-svg-PjHJBQXcrWSskyDH .section-edge-10{stroke:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-PjHJBQXcrWSskyDH .edge-depth-10{stroke-width:-16;}#mermaid-svg-PjHJBQXcrWSskyDH .section-10 line{stroke:hsl(30, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-PjHJBQXcrWSskyDH .disabled,#mermaid-svg-PjHJBQXcrWSskyDH .disabled circle,#mermaid-svg-PjHJBQXcrWSskyDH .disabled text{fill:lightgray;}#mermaid-svg-PjHJBQXcrWSskyDH .disabled text{fill:#efefef;}#mermaid-svg-PjHJBQXcrWSskyDH .section-root rect,#mermaid-svg-PjHJBQXcrWSskyDH .section-root path,#mermaid-svg-PjHJBQXcrWSskyDH .section-root circle,#mermaid-svg-PjHJBQXcrWSskyDH .section-root polygon{fill:hsl(240, 100%, 46.2745098039%);}#mermaid-svg-PjHJBQXcrWSskyDH .section-root text{fill:#ffffff;}#mermaid-svg-PjHJBQXcrWSskyDH .section-root span{color:#ffffff;}#mermaid-svg-PjHJBQXcrWSskyDH .section-2 span{color:#ffffff;}#mermaid-svg-PjHJBQXcrWSskyDH .icon-container{height:100%;display:flex;justify-content:center;align-items:center;}#mermaid-svg-PjHJBQXcrWSskyDH .edge{fill:none;}#mermaid-svg-PjHJBQXcrWSskyDH .mindmap-node-label{dy:1em;alignment-baseline:middle;text-anchor:middle;dominant-baseline:middle;text-align:center;}#mermaid-svg-PjHJBQXcrWSskyDH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} JavaScript正则与性能优化
正则表达式
原子与字符类
数量修饰符
位置修饰符
模式单元与分组
模式修饰符 i/g/m/s
NFA回溯引擎
ReDoS灾难性回溯
先行/后行断言 ES2018
命名捕获组 ES2018
JavaScript API
RegExp test/exec
String search/match/replace/split
matchAll 迭代器 ES2020
replaceAll ES2021
lastIndex陷阱
表单验证
blur实时验证
防抖包装input
密码强度检测
服务端二次校验
性能优化
防抖Debounce
setTimeout宏任务机制
leading leading边缘
节流Throttle
时间戳版
定时器版
混合版
rAF节流
与帧率同步
动画场景专用
【代码注释】思维导图总结全文知识体系:正则表达式从基础语法深入到 NFA 引擎原理与 ReDoS 安全;ES2018 命名捕获组/后行断言与 ES2020 matchAll 是现代 JS 正则最重要的新特性;性能优化章节从防抖节流原理延伸到事件循环机制与 rAF 动画节流,三大支柱形成完整技术栈。
高频面试题速查
- 字面量 vs 构造函数? 字面量
/pattern/加载时编译,固定规则优先;new RegExp()接收字符串,适合动态 pattern,注意双重转义\\d - 全局匹配
lastIndex陷阱? 带g同一实例连续test/exec会移动lastIndex;用matchAll或每次new RegExp()避免 match(/g/)vsmatchAll()?match(/g/)只返回字符串数组(丢弃分组);matchAll(/g/)返回迭代器,每项含index、groups(ES2020)- 什么是 ReDoS? 嵌套量词(如
(a+)+)触发 NFA 指数回溯,CPU 耗尽;避免方法:禁止嵌套量词、用具体字符类、加锚点 - 先行断言 vs 后行断言? 先行
(?=...)向右看(ES3);后行(?<=...)向左看(ES2018);均零宽不消耗字符 - 命名捕获组的优势?
(?<name>...)存入groups.name,比索引$1更语义化,重构不影响引用 - 防抖节流选择依据? 搜索/验证 → 防抖;scroll/动画 → 节流;拖拽/canvas → rAF 节流
- 防抖与事件循环?
clearTimeout取消宏任务,用户停止触发后 delay ms 才有一个 cb 进入执行栈 - rAF 节流 vs setTimeout 节流? rAF 与屏幕刷新率同步(自动 16.6ms 或 8.3ms),动画最流畅;页面不可见自动暂停
m修饰符作用? 多行模式下^$按行锚定(换行分隔的日志校验)- 防抖为何用
apply? 保留监听元素的this与arguments(含事件对象e) s修饰符(dotAll)? 让.匹配\n,用于跨行 HTML 内容匹配;之前用[\s\S]代替
验收自检清单
- 能写出
^1[3-9]\d{9}$并解释每段含义 - 区分贪婪
.*与懒惰.*? -
test/exec/match/replace/split各说一个用途 -
replace使用$1格式化手机号 - 封装
debounce、throttle且apply正确 - 两个
input对比有无防抖的请求次数 - 知道
^hello$/m与不加m的区别
常见错误排查表
| 现象 | 可能原因 | 处理 |
|---|---|---|
| 正则不生效 | 特殊字符未转义 | . * + ? 等加 \ |
| 手机号误通过 | 缺少 ^ $ |
整串匹配加锚点 |
replace 只换一处 |
未加 g |
/pattern/g |
全局 exec 死循环 |
lastIndex 未推进或 pattern 无匹配 |
检查循环条件 |
防抖 this 不对 |
未 apply |
method.apply(that, args) |
| 节流从不执行 | delay 过大或 prev 初值错误 |
用 Date.now() 初始化 |
| 多行校验失败 | 未加 m |
/^line$/m |
| 跨行 HTML 不匹配 | . 不含换行 |
加 s 标志或改 [\s\S]* |
matchAll 抛 TypeError |
正则缺 g 标志 |
确认 /pattern/g |
| 命名分组取值 undefined | groups 属性不存在 |
确认使用了 (?<name>...) 且结果不为 null |
| 动画节流卡顿 | setTimeout 间隔与帧率不对齐 |
改用 requestAnimationFrame 节流 |
| 用户输入构造 RegExp 崩溃 | 特殊字符未转义(注入) | 转义用户输入再 new RegExp |
学习建议
- 练习路径:按原子→量词→位置→分组→修饰符→JS API→表单→防抖节流顺序练习
- 工具推荐 :使用 RegEx101 在线调试,可直观看到 NFA 回溯步数;regex-vis.com 可视化有限自动机图
- 新特性实践 :用命名捕获组
(?<year>...)重写日期解析;用matchAll替换一个现有的while exec循环;体验s修饰符匹配多行 HTML - 安全意识 :对用户输入驱动的正则(如搜索关键词变 pattern),用
replace(/[.*+?^${}()|[\]\\]/g, '\\$&')转义后再构造 RegExp,防止注入;用 safe-regex 检测 ReDoS 风险 - 实战项目:注册表单 + 搜索防抖 + 滚动节流触底加载 + canvas 拖拽 rAF 节流
- 延伸方向:后端 Node.js 路由正则、爬虫数据提取、表单验证库(Yup / Zod / VeeValidate)的正则规则配置
相关资源: