Day24_JavaScript正则表达式与性能优化实战:从入门到精通

导读:本文系统讲解JavaScript正则表达式的完整知识体系,涵盖正则基础语法、JS中正则的应用、表单验证实战,以及高频事件处理中的防抖与节流技术。每个知识点均配完整可运行示例,适合已掌握JavaScript基础、希望深入理解字符串处理与性能优化的开发者。

目录


零、导读与学习价值

正则表达式是处理字符串的强大工具,广泛应用于表单验证、数据爬取、文本替换等场景。掌握正则表达式能极大提升字符串处理效率,配合防抖节流技术,能构建高性能的用户交互体验。

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

正则表达式由以下部分组成:

  1. 原子:基本匹配单位
  2. 数量修饰符:控制原子出现次数
  3. 位置修饰符:匹配字符串位置
  4. 模式单元:分组与捕获
  5. 模式修饰符:全局匹配模式

【实战要点】

  • 经典应用场景:表单验证(邮箱、手机号、身份证)、关键词高亮、数据清洗、日志分析
  • 常见坑 :忘记转义特殊字符.*+等;贪婪匹配导致过度匹配;未使用锚点导致部分匹配
  • 性能与最佳实践 :使用锚点^$快速失败;具体化字符类避免.;使用字面量而非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-regexvuln-regex-detector 等工具静态检测。服务端正则尤其重要,一个恶意请求可阻塞整个 Node.js 事件循环。

Q5:先行断言与后行断言的区别?命名捕获组的优势?

A:先行断言 (?=...)/(?!...) 向右看,ES3 起支持;后行断言 (?<=...)/(?<!...) 向左看,ES2018 才引入。两者都是零宽断言,不消耗字符。命名捕获组 (?<name>...) 的优势:① match.groups.namematch[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):返回首个匹配的索引,找不到返回-1
  • match(regexp):返回匹配结果数组或null
  • replace(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 的完整结果(含 indexgroupsinput)。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() 的完整结果(含 indexinputgroups)。需要提取捕获分组时必须用 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()方法返回布尔值判断匹配。理解此流程是构建健壮表单验证的基础。

验证策略:

  1. 用户名 :4-12位,由字母、数字、下划线组成 /^\w{4,12}$/
  2. 邮箱 :标准邮箱格式 /^[\w-]+@[\w-]+(\.\w+){1,2}$/
  3. 密码 :6-18位字符 /^.{6,18}$/
  4. 手机号 :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) 保证 changeInputthis 为当前 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)setTimeoutsetInterval、事件回调等;执行一个宏任务后检查微任务队列

概念与底层原理

防抖和节流是处理高频事件的核心优化技术:
#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传递正确的thisarguments;定时器未清理导致内存泄漏;节流时间间隔设置不合理
  • 性能与最佳实践 :使用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 动画节流,三大支柱形成完整技术栈。

高频面试题速查

  1. 字面量 vs 构造函数? 字面量 /pattern/ 加载时编译,固定规则优先;new RegExp() 接收字符串,适合动态 pattern,注意双重转义 \\d
  2. 全局匹配 lastIndex 陷阱?g 同一实例连续 test/exec 会移动 lastIndex;用 matchAll 或每次 new RegExp() 避免
  3. match(/g/) vs matchAll() match(/g/) 只返回字符串数组(丢弃分组);matchAll(/g/) 返回迭代器,每项含 indexgroups(ES2020)
  4. 什么是 ReDoS? 嵌套量词(如 (a+)+)触发 NFA 指数回溯,CPU 耗尽;避免方法:禁止嵌套量词、用具体字符类、加锚点
  5. 先行断言 vs 后行断言? 先行 (?=...) 向右看(ES3);后行 (?<=...) 向左看(ES2018);均零宽不消耗字符
  6. 命名捕获组的优势? (?<name>...) 存入 groups.name,比索引 $1 更语义化,重构不影响引用
  7. 防抖节流选择依据? 搜索/验证 → 防抖;scroll/动画 → 节流;拖拽/canvas → rAF 节流
  8. 防抖与事件循环? clearTimeout 取消宏任务,用户停止触发后 delay ms 才有一个 cb 进入执行栈
  9. rAF 节流 vs setTimeout 节流? rAF 与屏幕刷新率同步(自动 16.6ms 或 8.3ms),动画最流畅;页面不可见自动暂停
  10. m 修饰符作用? 多行模式下 ^ $ 按行锚定(换行分隔的日志校验)
  11. 防抖为何用 apply 保留监听元素的 thisarguments(含事件对象 e
  12. s 修饰符(dotAll)?. 匹配 \n,用于跨行 HTML 内容匹配;之前用 [\s\S] 代替

验收自检清单

  • 能写出 ^1[3-9]\d{9}$ 并解释每段含义
  • 区分贪婪 .* 与懒惰 .*?
  • test / exec / match / replace / split 各说一个用途
  • replace 使用 $1 格式化手机号
  • 封装 debouncethrottleapply 正确
  • 两个 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

学习建议

  1. 练习路径:按原子→量词→位置→分组→修饰符→JS API→表单→防抖节流顺序练习
  2. 工具推荐 :使用 RegEx101 在线调试,可直观看到 NFA 回溯步数;regex-vis.com 可视化有限自动机图
  3. 新特性实践 :用命名捕获组 (?<year>...) 重写日期解析;用 matchAll 替换一个现有的 while exec 循环;体验 s 修饰符匹配多行 HTML
  4. 安全意识 :对用户输入驱动的正则(如搜索关键词变 pattern),用 replace(/[.*+?^${}()|[\]\\]/g, '\\$&') 转义后再构造 RegExp,防止注入;用 safe-regex 检测 ReDoS 风险
  5. 实战项目:注册表单 + 搜索防抖 + 滚动节流触底加载 + canvas 拖拽 rAF 节流
  6. 延伸方向:后端 Node.js 路由正则、爬虫数据提取、表单验证库(Yup / Zod / VeeValidate)的正则规则配置

相关资源:

相关推荐
柚子科技1 小时前
Vue3 响应式原理:我被 ref 和 reactive 坑了3次后终于搞懂了
前端·javascript·vue.js
五月君_2 小时前
继 React、Vue 之后,Three.js 也有 Skills 了!AI 写 3D 终于不“晕”了
javascript·vue.js·人工智能·react.js·3d
scan7242 小时前
大模型只是知道要调用工具,本身不
前端·javascript·html
摇滚侠2 小时前
01 基础语法 JavaScript 入门到精通全套教程
开发语言·javascript·ecmascript
用户6919026813392 小时前
JS 初了解:从“网页玩具”到企业级语言的进化
javascript
月月大王的3D日记2 小时前
Three.js 材质篇(中):从兰伯特到PBR,一篇文章看懂五种光照材质
前端·javascript
且白2 小时前
leaflet切片变色、地图滤镜逻辑实现 colorfilter
前端·javascript
丷丩3 小时前
MapLibre GL JS第30课:添加视频
javascript·音视频·gis·mapbox·maplibre gl js
techdashen3 小时前
拆开任意 Electron 应用:从 Windows 安装包到 Discord 的私有更新协议
javascript·windows·electron